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 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 --- a/modular/sortable.core.esm.js
+++ b/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 */{ @@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
} }
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
@ -33,7 +24,7 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
} }
parentEl = el; // actualization parentEl = el; // actualization
@@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{ @@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
targetRect = getRect(target); targetRect = getRect(target);
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) { if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
capture(); capture();
@ -44,11 +35,10 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
+ catch(err) { + catch(err) {
+ return completed(false); + return completed(false);
+ } + }
+
parentEl = el; // actualization parentEl = el; // actualization
changed(); changed();
@@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{ @@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
_silent = true; _silent = true;
setTimeout(_unsilent, 30); setTimeout(_unsilent, 30);
capture(); capture();
@ -56,8 +46,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
- el.appendChild(dragEl); - el.appendChild(dragEl);
- } else { - } else {
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target); - target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
- }
+ try { + try {
+ if (after && !nextSibling) { + if (after && !nextSibling) {
+ el.appendChild(dragEl); + el.appendChild(dragEl);
@ -67,7 +55,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
+ } + }
+ catch(err) { + catch(err) {
+ return completed(false); + return completed(false);
+ } }
// Undo chrome's scroll adjustment (has no effect on other browsers) // 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 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 zopfliOptions = { threshold: 150 };
const compressDistBrotli = (rootDir, modernDir) => const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
gulp gulp
.src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], { .src(
base: rootDir, [
}) `${modernDir}/**/${filesGlob}`,
compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined,
].filter(Boolean),
{
base: rootDir,
}
)
.pipe(brotli(brotliOptions)) .pipe(brotli(brotliOptions))
.pipe(gulp.dest(rootDir)); .pipe(gulp.dest(rootDir));
const compressDistZopfli = (rootDir, modernDir) => const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
gulp gulp
.src( .src(
[ [
`${rootDir}/**/${filesGlob}`, `${rootDir}/**/${filesGlob}`,
`!${modernDir}/**/${filesGlob}`, compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
`!${rootDir}/{sw-modern,service_worker}.js`, `!${rootDir}/{sw-modern,service_worker}.js`,
`${rootDir}/{authorize,onboarding}.html`, `${rootDir}/{authorize,onboarding}.html`,
], ].filter(Boolean),
{ base: rootDir } { base: rootDir }
) )
.pipe(zopfli(zopfliOptions)) .pipe(zopfli(zopfliOptions))
@ -40,12 +46,20 @@ const compressDistZopfli = (rootDir, modernDir) =>
const compressAppBrotli = () => const compressAppBrotli = () =>
compressDistBrotli(paths.app_output_root, paths.app_output_latest); compressDistBrotli(paths.app_output_root, paths.app_output_latest);
const compressHassioBrotli = () => const compressHassioBrotli = () =>
compressDistBrotli(paths.hassio_output_root, paths.hassio_output_latest); compressDistBrotli(
paths.hassio_output_root,
paths.hassio_output_latest,
false
);
const compressAppZopfli = () => const compressAppZopfli = () =>
compressDistZopfli(paths.app_output_root, paths.app_output_latest); compressDistZopfli(paths.app_output_root, paths.app_output_latest);
const compressHassioZopfli = () => 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("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
gulp.task( gulp.task(

View File

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

View File

@ -111,9 +111,37 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature", 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": { "media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini", entity_id: "media_player.living_room_nest_mini",
state: "on", state: "playing",
attributes: { attributes: {
device_class: "speaker", device_class: "speaker",
volume_level: 0.18, volume_level: 0.18,

View File

@ -9,6 +9,22 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
title: isFrontpageEmbed ? "Home Assistant" : "Demo", title: isFrontpageEmbed ? "Home Assistant" : "Demo",
path: "home", path: "home",
icon: "mdi:home-assistant", 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: [ sections: [
...(isFrontpageEmbed ...(isFrontpageEmbed
? [] ? []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -234,7 +234,12 @@ export const SENSOR_ENTITIES = [
"weather", "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. /** 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 * 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 { stringCompare } from "../../common/string/compare";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by"; import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles"; import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer"; import { loadVirtualizer } from "../../resources/virtualizer";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
@ -35,6 +34,7 @@ import "../ha-svg-icon";
import "../search-input"; import "../search-input";
import { filterData, sortData } from "./sort-filter"; import { filterData, sortData } from "./sort-filter";
import { LocalizeFunc } from "../../common/translations/localize"; import { LocalizeFunc } from "../../common/translations/localize";
import { nextRender } from "../../common/util/render-status";
export interface RowClickedEvent { export interface RowClickedEvent {
id: string; id: string;
@ -169,8 +169,6 @@ export class HaDataTable extends LitElement {
@query("slot[name='header']") private _header!: HTMLSlotElement; @query("slot[name='header']") private _header!: HTMLSlotElement;
@state() private _items: DataTableRowData[] = [];
@state() private _collapsedGroups: string[] = []; @state() private _collapsedGroups: string[] = [];
private _checkableRowsCount?: number; private _checkableRowsCount?: number;
@ -179,7 +177,9 @@ export class HaDataTable extends LitElement {
private _sortColumns: SortableColumnContainer = {}; private _sortColumns: SortableColumnContainer = {};
private curRequest = 0; private _curRequest = 0;
private _lastUpdate = 0;
// @ts-ignore // @ts-ignore
@restoreScroll(".scroller") private _savedScrollPos?: number; @restoreScroll(".scroller") private _savedScrollPos?: number;
@ -206,9 +206,9 @@ export class HaDataTable extends LitElement {
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (this._items.length) { if (this._filteredData.length) {
// Force update of location of rows // 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("columns") ||
properties.has("_filter") || properties.has("_filter") ||
properties.has("sortColumn") || properties.has("sortColumn") ||
properties.has("sortDirection") || properties.has("sortDirection")
properties.has("groupColumn") ||
properties.has("groupOrder") ||
properties.has("_collapsedGroups")
) { ) {
this._sortFilterData(); this._sortFilterData();
} }
if (properties.has("selectable") || properties.has("hiddenColumns")) { 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 scroller
class="mdc-data-table__content scroller ha-scrollbar" class="mdc-data-table__content scroller ha-scrollbar"
@scroll=${this._saveScrollPos} @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} .keyFunction=${this._keyFunction}
.renderItem=${renderRow} .renderItem=${renderRow}
></lit-virtualizer> ></lit-virtualizer>
@ -602,8 +607,13 @@ export class HaDataTable extends LitElement {
private async _sortFilterData() { private async _sortFilterData() {
const startTime = new Date().getTime(); const startTime = new Date().getTime();
this.curRequest++; const timeBetweenUpdate = startTime - this._lastUpdate;
const curRequest = this.curRequest; const timeBetweenRequest = startTime - this._curRequest;
this._curRequest = startTime;
const forceUpdate =
!this._lastUpdate ||
(timeBetweenUpdate > 500 && timeBetweenRequest < 500);
let filteredData = this.data; let filteredData = this.data;
if (this._filter) { if (this._filter) {
@ -614,6 +624,10 @@ export class HaDataTable extends LitElement {
); );
} }
if (!forceUpdate && this._curRequest !== startTime) {
return;
}
const prom = this.sortColumn const prom = this.sortColumn
? sortData( ? sortData(
filteredData, filteredData,
@ -634,91 +648,103 @@ export class HaDataTable extends LitElement {
setTimeout(resolve, 100 - elapsed); setTimeout(resolve, 100 - elapsed);
}); });
} }
if (this.curRequest !== curRequest) {
if (!forceUpdate && this._curRequest !== startTime) {
return; return;
} }
const localize = this.localizeFunc || this.hass.localize; this._lastUpdate = startTime;
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._filteredData = data; 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( private _memFilterData = memoizeOne(
( (
data: DataTableRowData[], data: DataTableRowData[],
@ -802,8 +828,8 @@ export class HaDataTable extends LitElement {
private _checkedRowsChanged() { private _checkedRowsChanged() {
// force scroller to update, change it's items // force scroller to update, change it's items
if (this._items.length) { if (this._filteredData.length) {
this._items = [...this._items]; this._filteredData = [...this._filteredData];
} }
fireEvent(this, "selection-changed", { fireEvent(this, "selection-changed", {
value: this._checkedRows, value: this._checkedRows,

View File

@ -1,6 +1,6 @@
import { mdiTextureBox } from "@mdi/js"; import { mdiTextureBox } from "@mdi/js";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit"; import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
@ -20,12 +20,7 @@ import {
getDeviceEntityDisplayLookup, getDeviceEntityDisplayLookup,
} from "../data/device_registry"; } from "../data/device_registry";
import { EntityRegistryDisplayEntry } from "../data/entity_registry"; import { EntityRegistryDisplayEntry } from "../data/entity_registry";
import { import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry";
FloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant, ValueChangedEvent } from "../types"; import { HomeAssistant, ValueChangedEvent } from "../types";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-combo-box"; import "./ha-combo-box";
@ -50,7 +45,7 @@ interface FloorAreaEntry {
} }
@customElement("ha-area-floor-picker") @customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { export class HaAreaFloorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string; @property() public label?: string;
@ -111,22 +106,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@state() private _floors?: FloorRegistryEntry[];
@state() private _opened?: boolean; @state() private _opened?: boolean;
@query("ha-combo-box", true) public comboBox!: HaComboBox; @query("ha-combo-box", true) public comboBox!: HaComboBox;
private _init = false; private _init = false;
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
public async open() { public async open() {
await this.updateComplete; await this.updateComplete;
await this.comboBox?.open(); await this.comboBox?.open();
@ -431,12 +416,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if ( if (
(!this._init && this.hass && this._floors) || (!this._init && this.hass) ||
(this._init && changedProps.has("_opened") && this._opened) (this._init && changedProps.has("_opened") && this._opened)
) { ) {
this._init = true; this._init = true;
const areas = this._getAreas( const areas = this._getAreas(
this._floors!, Object.values(this.hass.floors),
Object.values(this.hass.areas), Object.values(this.hass.areas),
Object.values(this.hass.devices), Object.values(this.hass.devices),
Object.values(this.hass.entities), 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 "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
@ -15,13 +14,8 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { computeRTL } from "../common/util/compute_rtl"; import { computeRTL } from "../common/util/compute_rtl";
import { import { getFloorAreaLookup } from "../data/floor_registry";
FloorRegistryEntry,
getFloorAreaLookup,
subscribeFloorRegistry,
} from "../data/floor_registry";
import { RelatedResult, findRelated } from "../data/search"; import { RelatedResult, findRelated } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-check-list-item"; import "./ha-check-list-item";
@ -31,7 +25,7 @@ import "./ha-svg-icon";
import "./ha-tree-indicator"; import "./ha-tree-indicator";
@customElement("ha-filter-floor-areas") @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 hass!: HomeAssistant;
@property({ attribute: false }) public value?: { @property({ attribute: false }) public value?: {
@ -47,8 +41,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
@state() private _shouldRender = false; @state() private _shouldRender = false;
@state() private _floors?: FloorRegistryEntry[];
public willUpdate(properties: PropertyValues) { public willUpdate(properties: PropertyValues) {
super.willUpdate(properties); super.willUpdate(properties);
@ -60,7 +52,7 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
} }
protected render() { protected render() {
const areas = this._areas(this.hass.areas, this._floors); const areas = this._areas(this.hass.areas, this.hass.floors);
return html` return html`
<ha-expansion-panel <ha-expansion-panel
@ -189,14 +181,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
this._findRelated(); this._findRelated();
} }
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
return [
subscribeFloorRegistry(this.hass.connection, (floors) => {
this._floors = floors;
}),
];
}
protected updated(changed) { protected updated(changed) {
if (changed.has("expanded") && this.expanded) { if (changed.has("expanded") && this.expanded) {
setTimeout(() => { setTimeout(() => {
@ -220,9 +204,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
} }
private _areas = memoizeOne( private _areas = memoizeOne(
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => { (areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
const areas = Object.values(areaReg); const areas = Object.values(areaReg);
const floors = Object.values(floorReg);
const floorAreaLookup = getFloorAreaLookup(areas); const floorAreaLookup = getFloorAreaLookup(areas);
const unassisgnedAreas = areas.filter( const unassisgnedAreas = areas.filter(

View File

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

View File

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

View File

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

View File

@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button";
import "./ha-menu"; import "./ha-menu";
import type { HaMenu } from "./ha-menu"; import type { HaMenu } from "./ha-menu";
@customElement("ha-button-menu-new") @customElement("ha-md-button-menu")
export class HaButtonMenuNew extends LitElement { export class HaMdButtonMenu extends LitElement {
protected readonly [FOCUS_TARGET]; protected readonly [FOCUS_TARGET];
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@ -84,6 +84,6 @@ export class HaButtonMenuNew extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { 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 { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
@customElement("ha-list-item-new") @customElement("ha-md-list-item")
export class HaListItemNew extends MdListItem { export class HaMdListItem extends MdListItem {
static override styles = [ static override styles = [
...super.styles, ...super.styles,
css` css`
@ -21,6 +21,6 @@ export class HaListItemNew extends MdListItem {
declare global { declare global {
interface HTMLElementTagNameMap { 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 { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
@customElement("ha-list-new") @customElement("ha-md-list")
export class HaListNew extends MdList { export class HaMdList extends MdList {
static override styles = [ static override styles = [
...super.styles, ...super.styles,
css` css`
@ -16,6 +16,6 @@ export class HaListNew extends MdList {
declare global { declare global {
interface HTMLElementTagNameMap { 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 { css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@customElement("ha-menu-item") @customElement("ha-md-menu-item")
export class HaMenuItem extends MdMenuItem { export class HaMdMenuItem extends MdMenuItem {
@property({ attribute: false }) clickAction?: (item?: HTMLElement) => void; @property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
static override styles = [ static override styles = [
@ -41,6 +41,6 @@ export class HaMenuItem extends MdMenuItem {
declare global { declare global {
interface HTMLElementTagNameMap { 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"; } from "@material/web/menu/internal/controllers/shared";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import type { HaMenuItem } from "./ha-menu-item"; import type { HaMdMenuItem } from "./ha-md-menu-item";
@customElement("ha-menu") @customElement("ha-menu")
export class HaMenu extends MdMenu { export class HaMenu extends MdMenu {
@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
) { ) {
return; return;
} }
(ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator); (ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
} }
static override styles = [ static override styles = [

View File

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

View File

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

View File

@ -240,12 +240,24 @@ export class HaServiceControl extends LitElement {
...value, ...value,
selector: value.selector as Selector | undefined, 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 { return {
...serviceDomains[domain][serviceName], ...serviceDomains[domain][serviceName],
fields, fields,
hasSelector: fields.length hasSelector,
? fields.filter((field) => field.selector).map((field) => field.key)
: [],
}; };
} }
); );

View File

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

View File

@ -109,7 +109,7 @@ export class HaTextField extends TextFieldBase {
color: var(--secondary-text-color); 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); 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 let candidates = ""; // Build an Offer SDP string with ice candidates
const iceResolver = new Promise<void>((resolve) => { const iceResolver = new Promise<void>((resolve) => {
peerConnection.addEventListener("icecandidate", async (event) => { peerConnection.addEventListener("icecandidate", async (event) => {
if (!event.candidate) { if (!event.candidate?.candidate) {
resolve(); // Gathering complete resolve(); // Gathering complete
return; return;
} }

View File

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

View File

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

View File

@ -68,9 +68,18 @@ export const describeTrigger = (
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
ignoreAlias = false ignoreAlias = false
) => { ): string => {
try { 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) { } catch (error: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);
@ -700,9 +709,18 @@ export const describeCondition = (
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
ignoreAlias = false ignoreAlias = false
) => { ): string => {
try { 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) { } catch (error: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);

View File

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

View File

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

View File

@ -33,6 +33,7 @@ export interface DeviceRegistryEntry extends RegistryEntry {
entry_type: "service" | null; entry_type: "service" | null;
disabled_by: "user" | "integration" | "config_entry" | null; disabled_by: "user" | "integration" | "config_entry" | null;
configuration_url: string | null; configuration_url: string | null;
primary_config_entry: string | null;
} }
export interface DeviceEntityDisplayLookup { 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 { stringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { AreaRegistryEntry } from "./area_registry"; import { AreaRegistryEntry } from "./area_registry";
import { RegistryEntry } from "./registry"; import { RegistryEntry } from "./registry";
@ -27,48 +24,6 @@ export interface FloorRegistryEntryMutableParams {
aliases?: string[]; 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 = ( export const createFloorRegistryEntry = (
hass: HomeAssistant, hass: HomeAssistant,
values: FloorRegistryEntryMutableParams values: FloorRegistryEntryMutableParams

View File

@ -2,6 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { subscribeDeviceRegistry } from "./device_registry"; import { subscribeDeviceRegistry } from "./device_registry";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
export enum NetworkType { export enum NetworkType {
THREAD = "thread", THREAD = "thread",
@ -51,10 +53,30 @@ export interface MatterCommissioningParameters {
export const canCommissionMatterExternal = (hass: HomeAssistant) => export const canCommissionMatterExternal = (hass: HomeAssistant) =>
hass.auth.external?.config.canCommissionMatter; hass.auth.external?.config.canCommissionMatter;
export const startExternalCommissioning = (hass: HomeAssistant) => export const startExternalCommissioning = async (hass: HomeAssistant) => {
hass.auth.external!.fireMessage({ 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", type: "matter/commission",
}); });
};
export const redirectOnNewMatterDevice = ( export const redirectOnNewMatterDevice = (
hass: HomeAssistant, hass: HomeAssistant,

View File

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

View File

@ -3,6 +3,7 @@ import { Context, HomeAssistant } from "../types";
import { import {
BlueprintAutomationConfig, BlueprintAutomationConfig,
ManualAutomationConfig, ManualAutomationConfig,
flattenTriggers,
} from "./automation"; } from "./automation";
import { BlueprintScriptConfig, ScriptConfig } from "./script"; import { BlueprintScriptConfig, ScriptConfig } from "./script";
@ -190,7 +191,11 @@ export const getDataFromPath = (
if (!tempResult && raw === "sequence") { if (!tempResult && raw === "sequence") {
continue; continue;
} }
result = tempResult; if (raw === "trigger") {
result = flattenTriggers(tempResult);
} else {
result = tempResult;
}
continue; 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; type: string;
unit: string; unit: string;
states: { [key: number]: string }; states: { [key: number]: string };
default: any;
} }
export interface ZWaveJSSetConfigParamData { 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 ( return (
hass.localize( hass.localize(

View File

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

View File

@ -1,15 +1,18 @@
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button"; 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-dialog-header";
import "../../../../components/ha-icon-button-toggle"; import "../../../../components/ha-icon-button-toggle";
import type { EntityRegistryEntry } from "../../../../data/entity_registry"; import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import { import {
formatTempColor,
LightColor, LightColor,
LightColorMode, LightColorMode,
LightEntity, LightEntity,
@ -38,15 +41,7 @@ class DialogLightColorFavorite extends LitElement {
@state() private _modes: LightPickerMode[] = []; @state() private _modes: LightPickerMode[] = [];
@state() private _currentValue?: string; @query("ha-md-dialog") private _dialog?: HaMdDialog;
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;
}
}
public async showDialog( public async showDialog(
dialogParams: LightColorFavoriteDialogParams dialogParams: LightColorFavoriteDialogParams
@ -58,10 +53,7 @@ class DialogLightColorFavorite extends LitElement {
} }
public closeDialog(): void { public closeDialog(): void {
this._dialogParams = undefined; this._dialog?.close();
this._entry = undefined;
this._color = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
private _updateModes() { private _updateModes() {
@ -130,9 +122,20 @@ class DialogLightColorFavorite extends LitElement {
private async _cancel() { private async _cancel() {
this._dialogParams?.cancel?.(); this._dialogParams?.cancel?.();
}
private _cancelDialog() {
this._cancel();
this.closeDialog(); this.closeDialog();
} }
private _dialogClosed(): void {
this._dialogParams = undefined;
this._entry = undefined;
this._color = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private async _save() { private async _save() {
if (!this._color) { if (!this._color) {
this._cancel(); this._cancel();
@ -156,82 +159,83 @@ class DialogLightColorFavorite extends LitElement {
} }
return html` return html`
<ha-dialog <ha-md-dialog
open open
@closed=${this._cancel} @cancel=${this._cancel}
.heading=${this._dialogParams?.title ?? ""} @closed=${this._dialogClosed}
flexContent aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
> >
<ha-dialog-header slot="heading"> <ha-dialog-header slot="headline">
<ha-icon-button <ha-icon-button
slot="navigationIcon" slot="navigationIcon"
dialogAction="cancel" @click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")} .label=${this.hass.localize("ui.common.close")}
.path=${mdiClose} .path=${mdiClose}
></ha-icon-button> ></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> </ha-dialog-header>
<div class="header"> <div slot="content">
<span class="value">${this._currentValue}</span> <div class="header">
${this._modes.length > 1 ${this._modes.length > 1
? html` ? html`
<div class="modes"> <div class="modes">
${this._modes.map( ${this._modes.map(
(value) => html` (value) => html`
<ha-icon-button-toggle <ha-icon-button-toggle
border-only border-only
.selected=${value === this._mode} .selected=${value === this._mode}
.label=${this.hass.localize( .label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}` `ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)} )}
.mode=${value} .mode=${value}
@click=${this._modeChanged} @click=${this._modeChanged}
> >
<span <span
class="wheel ${classMap({ [value]: true })}" class="wheel ${classMap({ [value]: true })}"
></span> ></span>
</ha-icon-button-toggle> </ha-icon-button-toggle>
` `
)} )}
</div> </div>
` `
: nothing} : 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>
<div slot="actions">
<div class="content"> <ha-button @click=${this._cancelDialog}>
${this._mode === "color_temp" ${this.hass.localize("ui.common.cancel")}
? html` </ha-button>
<light-color-temp-picker <ha-button @click=${this._save} .disabled=${!this._color}
.hass=${this.hass} >${this.hass.localize("ui.common.save")}</ha-button
.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> </div>
<ha-button slot="secondaryAction" dialogAction="cancel"> </ha-md-dialog>
${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>
`; `;
} }
@ -239,19 +243,23 @@ class DialogLightColorFavorite extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-dialog { ha-md-dialog {
--dialog-content-padding: 0; 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) { @media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog { ha-md-dialog {
--dialog-surface-margin-top: 100px; min-width: 100%;
--mdc-dialog-min-height: auto; min-height: auto;
--mdc-dialog-max-height: calc(100% - 100px); max-height: calc(100% - 100px);
--ha-dialog-border-radius: var( margin-bottom: 0;
--ha-dialog-bottom-sheet-border-radius,
28px 28px 0 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% 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 { computeObjectId } from "../../../common/entity/compute_object_id";
import { listenMediaQuery } from "../../../common/dom/media_query"; import { listenMediaQuery } from "../../../common/dom/media_query";
import "../components/ha-more-info-state-header"; import "../components/ha-more-info-state-header";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
@customElement("more-info-script") @customElement("more-info-script")
class MoreInfoScript extends LitElement { class MoreInfoScript extends LitElement {
@ -28,6 +29,8 @@ class MoreInfoScript extends LitElement {
@property({ attribute: false }) public stateObj?: ScriptEntity; @property({ attribute: false }) public stateObj?: ScriptEntity;
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
@state() private _scriptData: Record<string, any> = {}; @state() private _scriptData: Record<string, any> = {};
@state() private narrow = false; @state() private narrow = false;
@ -59,8 +62,9 @@ class MoreInfoScript extends LitElement {
const stateObj = this.stateObj; const stateObj = this.stateObj;
const fields = const fields =
this.hass.services.script[computeObjectId(this.stateObj.entity_id)] this.hass.services.script[
?.fields; this.entry?.unique_id || computeObjectId(this.stateObj.entity_id)
]?.fields;
const hasFields = fields && Object.keys(fields).length > 0; const hasFields = fields && Object.keys(fields).length > 0;
@ -138,17 +142,30 @@ class MoreInfoScript extends LitElement {
protected override willUpdate(changedProperties: PropertyValues): void { protected override willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties); super.willUpdate(changedProperties);
if (!changedProperties.has("stateObj")) { if (changedProperties.has("stateObj")) {
return; 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 if (this.entry?.unique_id && changedProperties.has("entry")) {
| HassEntity const action = `script.${this.entry?.unique_id}`;
| undefined; if (this._scriptData?.action !== action) {
const newState = this.stateObj; this._scriptData = { ...this._scriptData, action };
}
if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) {
this._scriptData = { action: newState.entity_id, data: {} };
} }
} }
@ -161,7 +178,7 @@ class MoreInfoScript extends LitElement {
ev.stopPropagation(); ev.stopPropagation();
this.hass.callService( this.hass.callService(
"script", "script",
computeObjectId(this.stateObj!.entity_id), this.entry?.unique_id || computeObjectId(this.stateObj!.entity_id),
this._scriptData.data this._scriptData.data
); );
} }

View File

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

View File

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

View File

@ -35,10 +35,10 @@ import type {
HaDataTable, HaDataTable,
SortingDirection, SortingDirection,
} from "../components/data-table/ha-data-table"; } from "../components/data-table/ha-data-table";
import "../components/ha-button-menu-new"; import "../components/ha-md-button-menu";
import "../components/ha-dialog"; import "../components/ha-dialog";
import { HaMenu } from "../components/ha-menu"; import { HaMenu } from "../components/ha-menu";
import "../components/ha-menu-item"; import "../components/ha-md-menu-item";
import "../components/search-input-outlined"; import "../components/search-input-outlined";
import type { HomeAssistant, Route } from "../types"; import type { HomeAssistant, Route } from "../types";
import "./hass-tabs-subpage"; import "./hass-tabs-subpage";
@ -330,7 +330,7 @@ export class HaTabsSubpageDataTable extends LitElement {
"ui.components.subpage-data-table.exit_selection_mode" "ui.components.subpage-data-table.exit_selection_mode"
)} )}
></ha-icon-button> ></ha-icon-button>
<ha-button-menu-new positioning="absolute"> <ha-md-button-menu positioning="absolute">
<ha-assist-chip <ha-assist-chip
.label=${localize( .label=${localize(
"ui.components.subpage-data-table.select" "ui.components.subpage-data-table.select"
@ -346,20 +346,26 @@ export class HaTabsSubpageDataTable extends LitElement {
.path=${mdiMenuDown} .path=${mdiMenuDown}
></ha-svg-icon ></ha-svg-icon
></ha-assist-chip> ></ha-assist-chip>
<ha-menu-item .value=${undefined} @click=${this._selectAll}> <ha-md-menu-item
.value=${undefined}
@click=${this._selectAll}
>
<div slot="headline"> <div slot="headline">
${localize("ui.components.subpage-data-table.select_all")} ${localize("ui.components.subpage-data-table.select_all")}
</div> </div>
</ha-menu-item> </ha-md-menu-item>
<ha-menu-item .value=${undefined} @click=${this._selectNone}> <ha-md-menu-item
.value=${undefined}
@click=${this._selectNone}
>
<div slot="headline"> <div slot="headline">
${localize( ${localize(
"ui.components.subpage-data-table.select_none" "ui.components.subpage-data-table.select_none"
)} )}
</div> </div>
</ha-menu-item> </ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider> <md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item <ha-md-menu-item
.value=${undefined} .value=${undefined}
@click=${this._disableSelectMode} @click=${this._disableSelectMode}
> >
@ -368,8 +374,8 @@ export class HaTabsSubpageDataTable extends LitElement {
"ui.components.subpage-data-table.close_select_mode" "ui.components.subpage-data-table.close_select_mode"
)} )}
</div> </div>
</ha-menu-item> </ha-md-menu-item>
</ha-button-menu-new> </ha-md-button-menu>
<p> <p>
${localize("ui.components.subpage-data-table.selected", { ${localize("ui.components.subpage-data-table.selected", {
selected: this.selected || "0", selected: this.selected || "0",
@ -476,27 +482,27 @@ export class HaTabsSubpageDataTable extends LitElement {
${Object.entries(this.columns).map(([id, column]) => ${Object.entries(this.columns).map(([id, column]) =>
column.groupable column.groupable
? html` ? html`
<ha-menu-item <ha-md-menu-item
.value=${id} .value=${id}
@click=${this._handleGroupBy} @click=${this._handleGroupBy}
.selected=${id === this._groupColumn} .selected=${id === this._groupColumn}
class=${classMap({ selected: id === this._groupColumn })} class=${classMap({ selected: id === this._groupColumn })}
> >
${column.title || column.label} ${column.title || column.label}
</ha-menu-item> </ha-md-menu-item>
` `
: nothing : nothing
)} )}
<ha-menu-item <ha-md-menu-item
.value=${undefined} .value=${undefined}
@click=${this._handleGroupBy} @click=${this._handleGroupBy}
.selected=${this._groupColumn === undefined} .selected=${this._groupColumn === undefined}
class=${classMap({ selected: this._groupColumn === undefined })} class=${classMap({ selected: this._groupColumn === undefined })}
> >
${localize("ui.components.subpage-data-table.dont_group_by")} ${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-menu-item> </ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider> <md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item <ha-md-menu-item
@click=${this._collapseAllGroups} @click=${this._collapseAllGroups}
.disabled=${this._groupColumn === undefined} .disabled=${this._groupColumn === undefined}
> >
@ -505,8 +511,8 @@ export class HaTabsSubpageDataTable extends LitElement {
.path=${mdiUnfoldLessHorizontal} .path=${mdiUnfoldLessHorizontal}
></ha-svg-icon> ></ha-svg-icon>
${localize("ui.components.subpage-data-table.collapse_all_groups")} ${localize("ui.components.subpage-data-table.collapse_all_groups")}
</ha-menu-item> </ha-md-menu-item>
<ha-menu-item <ha-md-menu-item
@click=${this._expandAllGroups} @click=${this._expandAllGroups}
.disabled=${this._groupColumn === undefined} .disabled=${this._groupColumn === undefined}
> >
@ -515,13 +521,13 @@ export class HaTabsSubpageDataTable extends LitElement {
.path=${mdiUnfoldMoreHorizontal} .path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon> ></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")} ${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-menu-item> </ha-md-menu-item>
</ha-menu> </ha-menu>
<ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed"> <ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
${Object.entries(this.columns).map(([id, column]) => ${Object.entries(this.columns).map(([id, column]) =>
column.sortable column.sortable
? html` ? html`
<ha-menu-item <ha-md-menu-item
.value=${id} .value=${id}
@click=${this._handleSortBy} @click=${this._handleSortBy}
keep-open keep-open
@ -539,7 +545,7 @@ export class HaTabsSubpageDataTable extends LitElement {
` `
: nothing} : nothing}
${column.title || column.label} ${column.title || column.label}
</ha-menu-item> </ha-md-menu-item>
` `
: nothing : nothing
)} )}
@ -893,7 +899,7 @@ export class HaTabsSubpageDataTable extends LitElement {
#sort-by-anchor, #sort-by-anchor,
#group-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; --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 { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
CSSResultGroup, CSSResultGroup,
@ -13,6 +12,7 @@ import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-button";
import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries"; import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
import { subscribeConfigFlowInProgress } from "../data/config_flow"; import { subscribeConfigFlowInProgress } from "../data/config_flow";
import { domainToName } from "../data/integration"; import { domainToName } from "../data/integration";
@ -117,6 +117,30 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
const foundIntegrations = domains.length; 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) { if (domains.length > 12) {
domains = domains.slice(0, 11); domains = domains.slice(0, 11);
} }
@ -149,11 +173,11 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
: nothing} : nothing}
</div> </div>
<div class="footer"> <div class="footer">
<mwc-button unelevated @click=${this._finish}> <ha-button unelevated @click=${this._finish}>
${this.onboardingLocalize( ${this.onboardingLocalize(
"ui.panel.page-onboarding.integration.finish" "ui.panel.page-onboarding.integration.finish"
)} )}
</mwc-button> </ha-button>
</div> </div>
`; `;
} }
@ -193,6 +217,10 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
align-items: center; align-items: center;
height: 100%; height: 100%;
} }
.all-set-icon {
font-size: 64px;
text-align: center;
}
`, `,
]; ];
} }

View File

@ -6,7 +6,6 @@ import {
mdiPencil, mdiPencil,
mdiPlus, mdiPlus,
} from "@mdi/js"; } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
@ -15,15 +14,15 @@ import {
html, html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { formatListWithAnds } from "../../../common/string/format-list"; import { formatListWithAnds } from "../../../common/string/format-list";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-floor-icon"; import "../../../components/ha-floor-icon";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-sortable"; import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import { import {
AreaRegistryEntry, AreaRegistryEntry,
createAreaRegistryEntry, createAreaRegistryEntry,
@ -34,7 +33,6 @@ import {
createFloorRegistryEntry, createFloorRegistryEntry,
deleteFloorRegistryEntry, deleteFloorRegistryEntry,
getFloorAreaLookup, getFloorAreaLookup,
subscribeFloorRegistry,
updateFloorRegistryEntry, updateFloorRegistryEntry,
} from "../../../data/floor_registry"; } from "../../../data/floor_registry";
import { import {
@ -42,7 +40,6 @@ import {
showConfirmationDialog, showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section"; import "../ha-config-section";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
@ -57,7 +54,7 @@ const UNASSIGNED_PATH = ["__unassigned__"];
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true }; const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
@customElement("ha-config-areas-dashboard") @customElement("ha-config-areas-dashboard")
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false; @property({ type: Boolean }) public isWide = false;
@ -66,14 +63,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@state() private _floors?: FloorRegistryEntry[];
private _processAreas = memoizeOne( private _processAreas = memoizeOne(
( (
areas: HomeAssistant["areas"], areas: HomeAssistant["areas"],
devices: HomeAssistant["devices"], devices: HomeAssistant["devices"],
entities: HomeAssistant["entities"], entities: HomeAssistant["entities"],
floors: FloorRegistryEntry[] floors: HomeAssistant["floors"]
) => { ) => {
const processArea = (area: AreaRegistryEntry) => { const processArea = (area: AreaRegistryEntry) => {
let noDevicesInArea = 0; let noDevicesInArea = 0;
@ -109,7 +104,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
(area) => !area.floor_id || !floorAreaLookup[area.floor_id] (area) => !area.floor_id || !floorAreaLookup[area.floor_id]
); );
return { return {
floors: floors.map((floor) => ({ floors: Object.values(floors).map((floor) => ({
...floor, ...floor,
areas: (floorAreaLookup[floor.floor_id] || []).map(processArea), 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 { protected render(): TemplateResult {
const areasAndFloors = const areasAndFloors =
!this.hass.areas || !this.hass.areas ||
!this.hass.devices || !this.hass.devices ||
!this.hass.entities || !this.hass.entities ||
!this._floors !this.hass.floors
? undefined ? undefined
: this._processAreas( : this._processAreas(
this.hass.areas, this.hass.areas,
this.hass.devices, this.hass.devices,
this.hass.entities, this.hass.entities,
this._floors this.hass.floors
); );
return html` return html`
@ -327,7 +314,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
this.hass.areas, this.hass.areas,
this.hass.devices, this.hass.devices,
this.hass.entities, this.hass.entities,
this._floors! this.hass.floors
); );
let area: AreaRegistryEntry; let area: AreaRegistryEntry;
if (ev.detail.oldPath === UNASSIGNED_PATH) { 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";
import "../../../components/ha-icon-button-prev"; import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../../components/ha-list-item-new"; import "../../../components/ha-md-list-item";
import "../../../components/ha-list-new"; import "../../../components/ha-md-list";
import "../../../components/ha-service-icon"; import "../../../components/ha-service-icon";
import "../../../components/search-input"; import "../../../components/search-input";
import { import {
@ -434,7 +434,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
protected _opened(): void { protected _opened(): void {
// Store the width and height so that when we search, box doesn't jump // Store the width and height so that when we search, box doesn't jump
const boundingRect = const boundingRect =
this.shadowRoot!.querySelector("ha-list-new")?.getBoundingClientRect(); this.shadowRoot!.querySelector("ha-md-list")?.getBoundingClientRect();
this._width = boundingRect?.width; this._width = boundingRect?.width;
this._height = boundingRect?.height; this._height = boundingRect?.height;
} }
@ -526,7 +526,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
)} )}
></search-input> ></search-input>
</div> </div>
<ha-list-new <ha-md-list
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)} dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
style=${styleMap({ style=${styleMap({
width: this._width ? `${this._width}px` : "auto", width: this._width ? `${this._width}px` : "auto",
@ -537,7 +537,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
!this._filter && !this._filter &&
(!this._group || (!this._group ||
items.find((item) => item.key === this._params!.clipboardItem)) items.find((item) => item.key === this._params!.clipboardItem))
? html`<ha-list-item-new ? html`<ha-md-list-item
interactive interactive
type="button" type="button"
class="paste" class="paste"
@ -558,14 +558,14 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
.path=${mdiContentPaste} .path=${mdiContentPaste}
></ha-svg-icon ></ha-svg-icon
><ha-svg-icon slot="end" .path=${mdiPlus}></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>` <md-divider role="separator" tabindex="-1"></md-divider>`
: ""} : ""}
${repeat( ${repeat(
items, items,
(item) => item.key, (item) => item.key,
(item) => html` (item) => html`
<ha-list-item-new <ha-md-list-item
interactive interactive
type="button" type="button"
.value=${item.key} .value=${item.key}
@ -588,10 +588,10 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
slot="end" slot="end"
.path=${mdiPlus} .path=${mdiPlus}
></ha-svg-icon>`} ></ha-svg-icon>`}
</ha-list-item-new> </ha-md-list-item>
` `
)} )}
</ha-list-new> </ha-md-list>
</ha-dialog> </ha-dialog>
`; `;
} }
@ -643,13 +643,13 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
ha-icon-next { ha-icon-next {
width: 24px; width: 24px;
} }
ha-list-new { ha-md-list {
max-height: 468px; max-height: 468px;
max-width: 100vw; max-width: 100vw;
--md-list-item-leading-space: 24px; --md-list-item-leading-space: 24px;
--md-list-item-trailing-space: 24px; --md-list-item-trailing-space: 24px;
} }
ha-list-item-new img { ha-md-list-item img {
width: 24px; width: 24px;
} }
search-input { search-input {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,22 +35,17 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query"; import { listenMediaQuery } from "../../common/dom/media_query";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import { import { fullEntitiesContext, labelsContext } from "../../data/context";
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import { import {
entityRegistryByEntityId, entityRegistryByEntityId,
entityRegistryById, entityRegistryById,
subscribeEntityRegistry, subscribeEntityRegistry,
} from "../../data/entity_registry"; } from "../../data/entity_registry";
import { subscribeLabelRegistry } from "../../data/label_registry";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../types"; import { HomeAssistant, Route } from "../../types";
import { subscribeLabelRegistry } from "../../data/label_registry";
import { subscribeFloorRegistry } from "../../data/floor_registry";
declare global { declare global {
// for fire event // for fire event
@ -390,11 +385,6 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
initialValue: [], initialValue: [],
}); });
private _floorsContext = new ContextProvider(this, {
context: floorsContext,
initialValue: [],
});
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeEntityRegistry(this.hass.connection!, (entities) => { subscribeEntityRegistry(this.hass.connection!, (entities) => {
@ -403,9 +393,6 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
subscribeLabelRegistry(this.hass.connection!, (labels) => { subscribeLabelRegistry(this.hass.connection!, (labels) => {
this._labelsContext.setValue(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( const categoryItems = html`${this._categories?.map(
(category) => (category) =>
html`<ha-menu-item html`<ha-md-menu-item
.value=${category.category_id} .value=${category.category_id}
@click=${this._handleBulkCategory} @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-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`} : html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div> <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"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category" "ui.panel.config.automation.picker.bulk_actions.no_category"
)} )}
</div> </div>
</ha-menu-item> </ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider> <md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}> <ha-md-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline"> <div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")} ${this.hass.localize("ui.panel.config.category.editor.add")}
</div> </div>
</ha-menu-item>`; </ha-md-menu-item>`;
const labelItems = html`${this._labels?.map((label) => { const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) => const selected = this._selected.every((entityId) =>
@ -512,7 +512,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._selected.some((entityId) => this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id) this.hass.entities[entityId]?.labels.includes(label.label_id)
); );
return html`<ha-menu-item return html`<ha-md-menu-item
.value=${label.label_id} .value=${label.label_id}
.action=${selected ? "remove" : "add"} .action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel} @click=${this._handleBulkLabel}
@ -530,13 +530,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
: nothing} : nothing}
${label.name} ${label.name}
</ha-label> </ha-label>
</ha-menu-item> `; </ha-md-menu-item> `;
})}<md-divider role="separator" tabindex="-1"></md-divider> })}<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}> <ha-md-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline"> <div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div> </div>
</ha-menu-item>`; </ha-md-menu-item>`;
const labelsInOverflow = const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || (this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!this._sizeController.value && this.hass.dockedSidebar === "docked");
@ -637,7 +637,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-filter-categories> ></ha-filter-categories>
${!this.narrow ${!this.narrow
? html`<ha-button-menu-new slot="selection-bar"> ? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@ -650,10 +650,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${categoryItems} ${categoryItems}
</ha-button-menu-new> </ha-md-button-menu>
${labelsInOverflow ${labelsInOverflow
? nothing ? nothing
: html`<ha-button-menu-new slot="selection-bar"> : html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@ -666,11 +666,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${labelItems} ${labelItems}
</ha-button-menu-new>`}` </ha-md-button-menu>`}`
: nothing} : nothing}
${this.narrow || labelsInOverflow ${this.narrow || labelsInOverflow
? html` ? html`
<ha-button-menu-new has-overflow slot="selection-bar"> <ha-md-button-menu has-overflow slot="selection-bar">
${ ${
this.narrow this.narrow
? html`<ha-assist-chip ? html`<ha-assist-chip
@ -698,7 +698,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
${ ${
this.narrow this.narrow
? html`<ha-sub-menu> ? html`<ha-sub-menu>
<ha-menu-item slot="item"> <ha-md-menu-item slot="item">
<div slot="headline"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category" "ui.panel.config.automation.picker.bulk_actions.move_category"
@ -708,7 +708,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
slot="end" slot="end"
.path=${mdiChevronRight} .path=${mdiChevronRight}
></ha-svg-icon> ></ha-svg-icon>
</ha-menu-item> </ha-md-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu> <ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
@ -716,7 +716,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
${ ${
this.narrow || this.hass.dockedSidebar === "docked" this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu> ? html` <ha-sub-menu>
<ha-menu-item slot="item"> <ha-md-menu-item slot="item">
<div slot="headline"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label" "ui.panel.config.automation.picker.bulk_actions.add_label"
@ -726,12 +726,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
slot="end" slot="end"
.path=${mdiChevronRight} .path=${mdiChevronRight}
></ha-svg-icon> ></ha-svg-icon>
</ha-menu-item> </ha-md-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu> <ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
} }
</ha-button-menu-new>` </ha-md-button-menu>`
: nothing} : nothing}
<ha-integration-overflow-menu <ha-integration-overflow-menu
@ -1155,7 +1155,7 @@ ${rejected
ha-assist-chip { ha-assist-chip {
--ha-assist-chip-container-shape: 10px; --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; --md-assist-chip-trailing-space: 8px;
} }
ha-label { ha-label {

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ export const sharedStyles = css`
cursor: pointer; cursor: pointer;
text-decoration: underline; text-decoration: underline;
} }
ha-list-new { ha-md-list {
padding: 0; padding: 0;
--md-list-item-leading-space: var(--horizontal-padding, 16px); --md-list-item-leading-space: var(--horizontal-padding, 16px);
--md-list-item-trailing-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" "ui.panel.config.thread.add_dataset_from_tlv"
)}</mwc-list-item )}</mwc-list-item
> >
${!this._otbrInfo <mwc-list-item @click=${this._addOTBR}
? html`<mwc-list-item @click=${this._addOTBR} >${this.hass.localize(
>${this.hass.localize( "ui.panel.config.thread.add_open_thread_border_router"
"ui.panel.config.thread.add_open_thread_border_router" )}</mwc-list-item
)}</mwc-list-item >
>`
: ""}
</ha-button-menu> </ha-button-menu>
<div class="content"> <div class="content">
<h1>${this.hass.localize("ui.panel.config.thread.my_network")}</h1> <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", type: "thread/store_in_platform_keychain",
payload: { payload: {
mac_extended_address: otbr.extended_address, mac_extended_address: otbr.extended_address,
border_agent_id: otbr.border_agent_id ?? "", border_agent_id: otbr.border_agent_id,
active_operational_dataset: otbr.active_dataset_tlvs ?? "", active_operational_dataset: otbr.active_dataset_tlvs,
}, },
}); });
} }

View File

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

View File

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

View File

@ -250,7 +250,7 @@ export class HaConfigLogs extends LitElement {
--mdc-theme-primary: var(--primary-text-color); --mdc-theme-primary: var(--primary-text-color);
--mdc-icon-size: 36px; --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-end: 0px;
margin-inline-start: 8px; margin-inline-start: 8px;
} }

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { mdiPlus } from "@mdi/js"; import { mdiDelete, mdiPlus } from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -109,6 +109,20 @@ export class HaConfigLovelaceRescources extends LitElement {
) || resource.type} ) || 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!); 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) { private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail; this._activeSorting = ev.detail;
} }

View File

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

View File

@ -1,16 +1,34 @@
import { html } from "lit"; import { html } from "lit";
import { DataEntryFlowStep } from "../../../data/data_entry_flow";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import { import {
RepairsIssue,
createRepairsFlow, createRepairsFlow,
deleteRepairsFlow, deleteRepairsFlow,
fetchRepairsFlow, fetchRepairsFlow,
handleRepairsFlowStep, handleRepairsFlowStep,
RepairsIssue,
} from "../../../data/repairs"; } from "../../../data/repairs";
import { import {
loadDataEntryFlowDialog, loadDataEntryFlowDialog,
showFlowDialog, showFlowDialog,
} from "../../../dialogs/config-flow/show-dialog-data-entry-flow"; } 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; export const loadRepairFlowDialog = loadDataEntryFlowDialog;
@ -53,10 +71,11 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.abort.${step.reason}`, }.fix_flow.abort.${step.reason}`,
step.description_placeholders mergePlaceholders(issue, step)
); );
return description return html`${renderIssueDescription(hass, issue)}
${description
? html` ? html`
<ha-markdown <ha-markdown
breaks breaks
@ -64,7 +83,7 @@ export const showRepairsFlowDialog = (
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
` `
: step.reason; : step.reason}`;
}, },
renderShowFormStepHeader(hass, step) { renderShowFormStepHeader(hass, step) {
@ -73,7 +92,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.title`, }.fix_flow.step.${step.step_id}.title`,
step.description_placeholders mergePlaceholders(issue, step)
) || hass.localize("ui.dialogs.repair_flow.form.header") ) || hass.localize("ui.dialogs.repair_flow.form.header")
); );
}, },
@ -83,9 +102,10 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.description`, }.fix_flow.step.${step.step_id}.description`,
step.description_placeholders mergePlaceholders(issue, step)
); );
return description return html`${renderIssueDescription(hass, issue)}
${description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allowsvg
@ -93,7 +113,7 @@ export const showRepairsFlowDialog = (
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
` `
: ""; : ""}`;
}, },
renderShowFormStepFieldLabel(hass, step, field, options) { renderShowFormStepFieldLabel(hass, step, field, options) {
@ -101,7 +121,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data.${field.name}`, }.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.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data_description.${field.name}`, }.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>` ? html`<ha-markdown breaks .content=${description}></ha-markdown>`
: ""; : ""}`;
}, },
renderShowFormStepFieldError(hass, step, error) { renderShowFormStepFieldError(hass, step, error) {
@ -122,7 +143,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.error.${error}`, }.fix_flow.error.${error}`,
step.description_placeholders mergePlaceholders(issue, step)
); );
}, },
@ -165,7 +186,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.step.${ `component.${issue.domain}.issues.step.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.${step.step_id}.title`, }.fix_flow.${step.step_id}.title`,
step.description_placeholders mergePlaceholders(issue, step)
) || hass.localize(`component.${issue.domain}.title`) ) || hass.localize(`component.${issue.domain}.title`)
); );
}, },
@ -175,9 +196,9 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.progress.${step.progress_action}`, }.fix_flow.progress.${step.progress_action}`,
step.description_placeholders mergePlaceholders(issue, step)
); );
return description return html`${renderIssueDescription(hass, issue)}${description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allowsvg
@ -185,7 +206,7 @@ export const showRepairsFlowDialog = (
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
` `
: ""; : ""}`;
}, },
renderMenuHeader(hass, step) { renderMenuHeader(hass, step) {
@ -194,7 +215,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.title`, }.fix_flow.step.${step.step_id}.title`,
step.description_placeholders mergePlaceholders(issue, step)
) || hass.localize(`component.${issue.domain}.title`) ) || hass.localize(`component.${issue.domain}.title`)
); );
}, },
@ -204,9 +225,10 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.description`, }.fix_flow.step.${step.step_id}.description`,
step.description_placeholders mergePlaceholders(issue, step)
); );
return description return html`${renderIssueDescription(hass, issue)}
${description
? html` ? html`
<ha-markdown <ha-markdown
allowsvg allowsvg
@ -214,7 +236,7 @@ export const showRepairsFlowDialog = (
.content=${description} .content=${description}
></ha-markdown> ></ha-markdown>
` `
: ""; : ""}`;
}, },
renderMenuOption(hass, step, option) { renderMenuOption(hass, step, option) {
@ -222,7 +244,7 @@ export const showRepairsFlowDialog = (
`component.${issue.domain}.issues.${ `component.${issue.domain}.issues.${
issue.translation_key || issue.issue_id issue.translation_key || issue.issue_id
}.fix_flow.step.${step.step_id}.menu_options.${option}`, }.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-filter-labels";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu-item"; import "../../../components/ha-md-menu-item";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu"; import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
@ -423,7 +423,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
protected render(): TemplateResult { protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map( const categoryItems = html`${this._categories?.map(
(category) => (category) =>
html`<ha-menu-item html`<ha-md-menu-item
.value=${category.category_id} .value=${category.category_id}
@click=${this._handleBulkCategory} @click=${this._handleBulkCategory}
> >
@ -431,21 +431,21 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>` ? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`} : html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div> <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"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category" "ui.panel.config.automation.picker.bulk_actions.no_category"
)} )}
</div> </div>
</ha-menu-item> </ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider> <md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateCategory}> <ha-md-menu-item @click=${this._bulkCreateCategory}>
<div slot="headline"> <div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")} ${this.hass.localize("ui.panel.config.category.editor.add")}
</div> </div>
</ha-menu-item>`; </ha-md-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => { const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
@ -457,7 +457,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._selected.some((entityId) => this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id) this.hass.entities[entityId]?.labels.includes(label.label_id)
); );
return html`<ha-menu-item return html`<ha-md-menu-item
.value=${label.label_id} .value=${label.label_id}
.action=${selected ? "remove" : "add"} .action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel} @click=${this._handleBulkLabel}
@ -475,18 +475,18 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: nothing} : nothing}
${label.name} ${label.name}
</ha-label> </ha-label>
</ha-menu-item>`; </ha-md-menu-item>`;
})} })}
<md-divider role="separator" tabindex="-1"></md-divider> <md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateLabel}> <ha-md-menu-item @click=${this._bulkCreateLabel}>
<div slot="headline"> <div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")} ${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( const areaItems = html`${Object.values(this.hass.areas).map(
(area) => (area) =>
html`<ha-menu-item html`<ha-md-menu-item
.value=${area.area_id} .value=${area.area_id}
@click=${this._handleBulkArea} @click=${this._handleBulkArea}
> >
@ -497,23 +497,23 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.path=${mdiTextureBox} .path=${mdiTextureBox}
></ha-svg-icon>`} ></ha-svg-icon>`}
<div slot="headline">${area.name}</div> <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"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area" "ui.panel.config.devices.picker.bulk_actions.no_area"
)} )}
</div> </div>
</ha-menu-item> </ha-md-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider> <md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}> <ha-md-menu-item @click=${this._bulkCreateArea}>
<div slot="headline"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area" "ui.panel.config.devices.picker.bulk_actions.add_area"
)} )}
</div> </div>
</ha-menu-item>`; </ha-md-menu-item>`;
const areasInOverflow = const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) || (this._sizeController.value && this._sizeController.value < 900) ||
@ -637,7 +637,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-filter-categories> ></ha-filter-categories>
${!this.narrow ${!this.narrow
? html`<ha-button-menu-new slot="selection-bar"> ? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@ -650,10 +650,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${categoryItems} ${categoryItems}
</ha-button-menu-new> </ha-md-button-menu>
${labelsInOverflow ${labelsInOverflow
? nothing ? nothing
: html`<ha-button-menu-new slot="selection-bar"> : html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@ -666,10 +666,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${labelItems} ${labelItems}
</ha-button-menu-new>`} </ha-md-button-menu>`}
${areasInOverflow ${areasInOverflow
? nothing ? nothing
: html`<ha-button-menu-new slot="selection-bar"> : html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@ -682,11 +682,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${areaItems} ${areaItems}
</ha-button-menu-new>`}` </ha-md-button-menu>`}`
: nothing} : nothing}
${this.narrow || areasInOverflow ${this.narrow || areasInOverflow
? html` ? html`
<ha-button-menu-new has-overflow slot="selection-bar"> <ha-md-button-menu has-overflow slot="selection-bar">
${ ${
this.narrow this.narrow
? html`<ha-assist-chip ? html`<ha-assist-chip
@ -714,7 +714,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${ ${
this.narrow this.narrow
? html`<ha-sub-menu> ? html`<ha-sub-menu>
<ha-menu-item slot="item"> <ha-md-menu-item slot="item">
<div slot="headline"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category" "ui.panel.config.automation.picker.bulk_actions.move_category"
@ -724,7 +724,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
slot="end" slot="end"
.path=${mdiChevronRight} .path=${mdiChevronRight}
></ha-svg-icon> ></ha-svg-icon>
</ha-menu-item> </ha-md-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu> <ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
@ -732,7 +732,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${ ${
this.narrow || labelsInOverflow this.narrow || labelsInOverflow
? html`<ha-sub-menu> ? html`<ha-sub-menu>
<ha-menu-item slot="item"> <ha-md-menu-item slot="item">
<div slot="headline"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label" "ui.panel.config.automation.picker.bulk_actions.add_label"
@ -742,7 +742,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
slot="end" slot="end"
.path=${mdiChevronRight} .path=${mdiChevronRight}
></ha-svg-icon> ></ha-svg-icon>
</ha-menu-item> </ha-md-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu> <ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
@ -750,7 +750,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${ ${
this.narrow || areasInOverflow this.narrow || areasInOverflow
? html`<ha-sub-menu> ? html`<ha-sub-menu>
<ha-menu-item slot="item"> <ha-md-menu-item slot="item">
<div slot="headline"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area" "ui.panel.config.devices.picker.bulk_actions.move_area"
@ -760,12 +760,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
slot="end" slot="end"
.path=${mdiChevronRight} .path=${mdiChevronRight}
></ha-svg-icon> ></ha-svg-icon>
</ha-menu-item> </ha-md-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu> <ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
} }
</ha-button-menu-new>` </ha-md-button-menu>`
: nothing} : nothing}
${!this.scenes.length ${!this.scenes.length
? html`<div class="empty" slot="empty"> ? html`<div class="empty" slot="empty">
@ -1204,7 +1204,7 @@ ${rejected
ha-assist-chip { ha-assist-chip {
--ha-assist-chip-container-shape: 10px; --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; --md-assist-chip-trailing-space: 8px;
} }
ha-label { ha-label {

View File

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

View File

@ -3,7 +3,6 @@ import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color"; import { 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 { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color"; import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-badge";
import "../../../components/ha-ripple"; import "../../../components/ha-ripple";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
@ -160,15 +160,14 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
if (!stateObj) { if (!stateObj) {
return html` return html`
<div class="badge error"> <ha-badge .label=${entityId} class="error">
<ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon> <ha-svg-icon
<span class="info"> slot="icon"
<span class="label">${entityId}</span> .hass=${this.hass}
<span class="content"> .path=${mdiAlertCircle}
${this.hass.localize("ui.badge.entity.not_found")} ></ha-svg-icon>
</span> ${this.hass.localize("ui.badge.entity.not_found")}
</span> </ha-badge>
</div>
`; `;
} }
@ -204,42 +203,32 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
const content = showState ? stateDisplay : showName ? name : undefined; const content = showState ? stateDisplay : showName ? name : undefined;
return html` return html`
<div <ha-badge
style=${styleMap(style)} .type=${this.hasAction ? "button" : "badge"}
class="badge ${classMap({
active,
"no-info": !showState && !showName,
"no-icon": !showIcon,
})}"
@action=${this._handleAction} @action=${this._handleAction}
.actionHandler=${actionHandler({ .actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action), hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action), hasDoubleClick: hasAction(this._config!.double_tap_action),
})} })}
role=${ifDefined(this.hasAction ? "button" : undefined)} .label=${label}
tabindex=${ifDefined(this.hasAction ? "0" : undefined)} .iconOnly=${!content}
style=${styleMap(style)}
class=${classMap({ active })}
> >
<ha-ripple .disabled=${!this.hasAction}></ha-ripple>
${showIcon ${showIcon
? imageUrl ? imageUrl
? html`<img src=${imageUrl} aria-hidden />` ? html`<img slot="icon" src=${imageUrl} aria-hidden />`
: html` : html`
<ha-state-icon <ha-state-icon
slot="icon"
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .stateObj=${stateObj}
.icon=${this._config.icon} .icon=${this._config.icon}
></ha-state-icon> ></ha-state-icon>
` `
: nothing} : nothing}
${content ${content}
? html` </ha-badge>
<span class="info">
${label ? html`<span class="label">${name}</span>` : nothing}
<span class="content">${content}</span>
</span>
`
: nothing}
</div>
`; `;
} }
@ -249,119 +238,15 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
:host { ha-badge {
--badge-color: var(--state-inactive-color); --badge-color: var(--state-inactive-color);
-webkit-tap-highlight-color: transparent;
} }
.badge.error { ha-badge.error {
--badge-color: var(--red-color); --badge-color: var(--red-color);
} }
.badge { ha-badge.active {
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 {
--badge-color: var(--primary-color); --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 { dump } from "js-yaml";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import "../../../components/ha-label-badge"; import "../../../components/ha-badge";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../custom-card-helpers"; import { showAlertDialog } from "../custom-card-helpers";
import { LovelaceBadge } from "../types"; import { LovelaceBadge } from "../types";
import { HuiEntityBadge } from "./hui-entity-badge";
import { ErrorBadgeConfig } from "./types"; import { ErrorBadgeConfig } from "./types";
export const createErrorBadgeElement = (config) => { export const createErrorBadgeElement = (config) => {
@ -55,41 +54,36 @@ export class HuiErrorBadge extends LitElement implements LovelaceBadge {
} }
return html` return html`
<button class="badge error" @click=${this._viewDetail}> <ha-badge
<ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon> class="error"
<ha-ripple></ha-ripple> @click=${this._viewDetail}
<span class="content"> type="button"
<span class="name">Error</span> label="Error"
<span class="state">${this._config.error}</span> >
</span> <ha-svg-icon slot="icon" .path=${mdiAlertCircle}></ha-svg-icon>
</button> <div class="content">${this._config.error}</div>
</ha-badge>
`; `;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return css`
HuiEntityBadge.styles, ha-badge {
css` --badge-color: var(--error-color);
.badge.error { --ha-card-border-color: var(--error-color);
--badge-color: var(--error-color); }
border-color: var(--badge-color); .content {
} max-width: 100px;
ha-svg-icon { overflow: hidden;
color: var(--badge-color); text-overflow: ellipsis;
} white-space: nowrap;
.state { }
max-width: 100px; pre {
overflow: hidden; font-family: var(--code-font-family, monospace);
text-overflow: ellipsis; white-space: break-spaces;
white-space: nowrap; user-select: text;
} }
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, html,
nothing, nothing,
} from "lit"; } from "lit";
import { mdiChevronRight } from "@mdi/js";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/chart/state-history-charts"; import "../../../components/chart/state-history-charts";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next";
import { import {
HistoryResult, HistoryResult,
computeHistory, computeHistory,
@ -209,9 +208,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
? html` ? html`
<h1 class="card-header"> <h1 class="card-header">
${this._config.title} ${this._config.title}
<a href=${configUrl} <a href=${configUrl}><ha-icon-next></ha-icon-next></a>
><ha-icon-button .path=${mdiChevronRight}></ha-icon-button
></a>
</h1> </h1>
` `
: nothing} : nothing}
@ -258,7 +255,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
justify-content: space-between; justify-content: space-between;
display: flex; display: flex;
} }
.card-header ha-icon-button { .card-header ha-icon-next {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
line-height: 24px; line-height: 24px;
color: var(--primary-text-color); color: var(--primary-text-color);

View File

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

View File

@ -498,5 +498,22 @@ export interface TileCardConfig extends LovelaceCardConfig {
hold_action?: ActionConfig; hold_action?: ActionConfig;
double_tap_action?: ActionConfig; double_tap_action?: ActionConfig;
icon_tap_action?: ActionConfig; icon_tap_action?: ActionConfig;
icon_hold_action?: ActionConfig;
icon_double_tap_action?: ActionConfig;
features?: LovelaceCardFeatureConfig[]; 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; position: relative;
color: var(--primary-text-color); color: var(--primary-text-color);
border-radius: 50%; border-radius: 50%;
padding: 12px; padding: 8px;
background: var(--secondary-background-color); background: var(--secondary-background-color);
--mdc-icon-size: 24px; --mdc-icon-size: 20px;
} }
.more { .more {
position: absolute; position: absolute;

View File

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

View File

@ -34,4 +34,21 @@ export const configElementStyle = css`
margin-top: 8px; margin-top: 8px;
display: block; 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 { HassEntity } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/ha-button"; import "../../../../components/ha-button";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-list-item"; import "../../../../components/ha-list-item";
@ -236,119 +235,108 @@ export class HuiCardFeaturesEditor extends LitElement {
); );
return html` return html`
<ha-expansion-panel outlined> ${supportedFeaturesType.length === 0 && this.features.length === 0
<h3 slot="header"> ? html`
<ha-svg-icon .path=${mdiListBox}></ha-svg-icon> <ha-alert type="info">
${this.hass!.localize("ui.panel.lovelace.editor.features.name")} ${this.hass!.localize(
</h3> "ui.panel.lovelace.editor.features.no_compatible_available"
<div class="content"> )}
${supportedFeaturesType.length === 0 && this.features.length === 0 </ha-alert>
? html` `
<ha-alert type="info"> : nothing}
${this.hass!.localize( <ha-sortable handle-selector=".handle" @item-moved=${this._featureMoved}>
"ui.panel.lovelace.editor.features.no_compatible_available" <div class="features">
)} ${repeat(
</ha-alert> this.features,
` (featureConf) => this._getKey(featureConf),
: nothing} (featureConf, index) => {
<ha-sortable const type = featureConf.type;
handle-selector=".handle" const supported = this._supportsFeatureType(type);
@item-moved=${this._featureMoved} const editable = this._isFeatureTypeEditable(type);
> return html`
<div class="features"> <div class="feature">
${repeat( <div class="handle">
this.features, <ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
(featureConf) => this._getKey(featureConf), </div>
(featureConf, index) => { <div class="feature-content">
const type = featureConf.type; <div>
const supported = this._supportsFeatureType(type); <span> ${this._getFeatureTypeLabel(type)} </span>
const editable = this._isFeatureTypeEditable(type); ${this.stateObj && !supported
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
? html` ? html`
<ha-icon-button <span class="secondary">
.label=${this.hass!.localize( ${this.hass!.localize(
`ui.panel.lovelace.editor.features.edit` "ui.panel.lovelace.editor.features.not_compatible"
)} )}
.path=${mdiPencil} </span>
class="edit-icon"
.index=${index}
@click=${this._editFeature}
.disabled=${!supported}
></ha-icon-button>
` `
: nothing} : 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>
`; </div>
} ${editable
)} ? html`
</div> <ha-icon-button
</ha-sortable> .label=${this.hass!.localize(
${supportedFeaturesType.length > 0 `ui.panel.lovelace.editor.features.edit`
? html` )}
<ha-button-menu .path=${mdiPencil}
fixed class="edit-icon"
@action=${this._addFeature} .index=${index}
@closed=${stopPropagation} @click=${this._editFeature}
> .disabled=${!supported}
<ha-button ></ha-icon-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} : nothing}
${customTypes.map( <ha-icon-button
(type) => html` .label=${this.hass!.localize(
<ha-list-item .value=${type}> `ui.panel.lovelace.editor.features.remove`
${this._getFeatureTypeLabel(type)} )}
</ha-list-item> .path=${mdiDelete}
` class="remove-icon"
)} .index=${index}
</ha-button-menu> @click=${this._removeFeature}
` ></ha-icon-button>
: nothing} </div>
`;
}
)}
</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; display: flex !important;
flex-direction: column; 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 { ha-button-menu {
margin-top: 8px; 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 { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { import {
any, any,
@ -12,11 +14,13 @@ import {
string, string,
} from "superstruct"; } from "superstruct";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event"; import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { import type {
HaFormSchema, HaFormSchema,
SchemaUnion, SchemaUnion,
} from "../../../../components/ha-form/types"; } from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { import {
LovelaceCardFeatureConfig, LovelaceCardFeatureConfig,
@ -27,6 +31,7 @@ import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor"; import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types"; import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor"; import "./hui-card-features-editor";
import type { FeatureType } from "./hui-card-features-editor"; import type { FeatureType } from "./hui-card-features-editor";
@ -93,22 +98,30 @@ export class HuiHumidifierCardEditor
return nothing; return nothing;
} }
const stateObj = this._config.entity return cache(
? this.hass.states[this._config.entity] this._subElementEditorConfig
: undefined; ? this._renderFeatureForm()
: this._renderForm()
);
}
if (this._subElementEditorConfig) { private _renderFeatureForm() {
return html` const entityId = this._config!.entity;
<hui-sub-element-editor return html`
.hass=${this.hass} <hui-sub-element-editor
.config=${this._subElementEditorConfig} .hass=${this.hass}
.context=${this._context(this._config.entity)} .config=${this._subElementEditorConfig}
@go-back=${this._goBack} .context=${this._context(entityId)}
@config-changed=${this.subElementChanged} @go-back=${this._goBack}
> @config-changed=${this.subElementChanged}
</hui-sub-element-editor> >
`; </hui-sub-element-editor>
} `;
}
private _renderForm() {
const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
return html` return html`
<ha-form <ha-form
@ -118,14 +131,24 @@ export class HuiHumidifierCardEditor
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
<hui-card-features-editor <ha-expansion-panel outlined>
.hass=${this.hass} <h3 slot="header">
.stateObj=${stateObj} <ha-svg-icon .path=${mdiListBox}></ha-svg-icon>
.featuresTypes=${COMPATIBLE_FEATURES_TYPES} ${this.hass!.localize(
.features=${this._config!.features ?? []} "ui.panel.lovelace.editor.card.generic.features"
@features-changed=${this._featuresChanged} )}
@edit-detail-element=${this._editDetailElement} </h3>
></hui-card-features-editor> <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() { static get styles() {
return css` return [
ha-form { configElementStyle,
display: block; css`
margin-bottom: 24px; 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) { if (ev.detail && ev.detail.entities) {
this._config = { ...this._config!, entities: 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! }); 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 { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { import {
any, any,
@ -12,11 +14,13 @@ import {
string, string,
} from "superstruct"; } from "superstruct";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event"; import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { import type {
HaFormSchema, HaFormSchema,
SchemaUnion, SchemaUnion,
} from "../../../../components/ha-form/types"; } from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { import {
LovelaceCardFeatureConfig, LovelaceCardFeatureConfig,
@ -27,6 +31,7 @@ import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor"; import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditSubElementEvent, SubElementEditorConfig } from "../types"; import { EditSubElementEvent, SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor"; import "./hui-card-features-editor";
import type { FeatureType } from "./hui-card-features-editor"; import type { FeatureType } from "./hui-card-features-editor";
@ -91,22 +96,29 @@ export class HuiThermostatCardEditor
return nothing; return nothing;
} }
const stateObj = this._config.entity return cache(
? this.hass.states[this._config.entity] this._subElementEditorConfig
: undefined; ? this._renderFeatureForm()
: this._renderForm()
);
}
if (this._subElementEditorConfig) { private _renderFeatureForm() {
return html` return html`
<hui-sub-element-editor <hui-sub-element-editor
.hass=${this.hass} .hass=${this.hass}
.config=${this._subElementEditorConfig} .config=${this._subElementEditorConfig}
.context=${this._context(this._config.entity)} .context=${this._context(this._config!.entity)}
@go-back=${this._goBack} @go-back=${this._goBack}
@config-changed=${this.subElementChanged} @config-changed=${this.subElementChanged}
> >
</hui-sub-element-editor> </hui-sub-element-editor>
`; `;
} }
private _renderForm() {
const entityId = this._config!.entity;
const stateObj = entityId ? this.hass!.states[entityId] : undefined;
return html` return html`
<ha-form <ha-form
@ -116,14 +128,24 @@ export class HuiThermostatCardEditor
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
<hui-card-features-editor <ha-expansion-panel outlined>
.hass=${this.hass} <h3 slot="header">
.stateObj=${stateObj} <ha-svg-icon .path=${mdiListBox}></ha-svg-icon>
.featuresTypes=${COMPATIBLE_FEATURES_TYPES} ${this.hass!.localize(
.features=${this._config!.features ?? []} "ui.panel.lovelace.editor.card.generic.features"
@features-changed=${this._featuresChanged} )}
@edit-detail-element=${this._editDetailElement} </h3>
></hui-card-features-editor> <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() { static get styles() {
return css` return [
ha-form { configElementStyle,
display: block; css`
margin-bottom: 24px; 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 { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { import {
any, any,
@ -14,11 +15,13 @@ import {
union, union,
} from "superstruct"; } from "superstruct";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event"; import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { import type {
HaFormSchema, HaFormSchema,
SchemaUnion, SchemaUnion,
} from "../../../../components/ha-form/types"; } from "../../../../components/ha-form/types";
import "../../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { import {
LovelaceCardFeatureConfig, LovelaceCardFeatureConfig,
@ -168,27 +171,31 @@ export class HuiTileCardEditor
return nothing; return nothing;
} }
const stateObj = this._config.entity return cache(
? this.hass.states[this._config.entity] this._subElementEditorConfig
: undefined; ? this._renderFeatureForm()
: this._renderForm()
const schema = this._schema(
this._config.entity,
this._config.hide_state ?? false
); );
}
if (this._subElementEditorConfig) { private _renderFeatureForm() {
return html` return html`
<hui-sub-element-editor <hui-sub-element-editor
.hass=${this.hass} .hass=${this.hass}
.config=${this._subElementEditorConfig} .config=${this._subElementEditorConfig}
.context=${this._context(this._config.entity)} .context=${this._context(this._config!.entity)}
@go-back=${this._goBack} @go-back=${this._goBack}
@config-changed=${this.subElementChanged} @config-changed=${this.subElementChanged}
> >
</hui-sub-element-editor> </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; const data = this._config;
@ -200,13 +207,23 @@ export class HuiTileCardEditor
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
<hui-card-features-editor <ha-expansion-panel outlined>
.hass=${this.hass} <h3 slot="header">
.stateObj=${stateObj} <ha-svg-icon .path=${mdiListBox}></ha-svg-icon>
.features=${this._config!.features ?? []} ${this.hass!.localize(
@features-changed=${this._featuresChanged} "ui.panel.lovelace.editor.card.generic.features"
@edit-detail-element=${this._editDetailElement} )}
></hui-card-features-editor> </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