mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-12 20:26:54 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8192f7fe50 | |||
| 144eee9a80 | |||
| 8e6222db70 | |||
| 7f26010e5c | |||
| 266bd1294c | |||
| 0385ca8076 | |||
| 02c65fc8cb | |||
| 49290d5c83 | |||
| 08aff3bfd7 | |||
| 455fa45b9c | |||
| 2e56a4ec4c | |||
| 76131ff09e | |||
| 89d8723c5a | |||
| 7bdb63a6fe | |||
| eed79f1797 | |||
| 76665009da | |||
| 6d7d08fddc | |||
| 77d4e6dc43 | |||
| 7345256b30 | |||
| e0d98e95fa | |||
| 17041044cf | |||
| 9a10cd7fa8 | |||
| fa354aed2a | |||
| c044d96712 | |||
| 1b736960b2 | |||
| e8c8047ff9 | |||
| 9376f4ce81 | |||
| 7befa9782a | |||
| 0186ec1265 | |||
| 641c444fdc | |||
| 93dd2a5dc8 | |||
| f7cf3d5b39 | |||
| b861543865 | |||
| e749956eaa | |||
| 5b0f0dade5 | |||
| f86d2753f7 | |||
| f3f549737f | |||
| d9929905b5 | |||
| 25487e373e | |||
| 2ff56d3eb7 | |||
| 6c4f7506b5 | |||
| 5755aebff6 | |||
| 76996ea3cc | |||
| d7d6766f80 | |||
| b632e8e6f8 | |||
| ee4eaaa613 | |||
| 395faebd0c | |||
| 71b8676e02 | |||
| d54516dd42 | |||
| 1a3eef9c4f | |||
| 1f2f9e6330 | |||
| 1774219f9a | |||
| ac66ad1a32 | |||
| 7bb51c746d | |||
| 13e32c41e0 | |||
| d89af52e3b | |||
| da6114fa5f | |||
| c144533834 | |||
| e6c6ab93ef | |||
| 62df56e5d9 | |||
| d169eb9c49 |
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -62,4 +62,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
|
||||
- uses: release-drafter/release-drafter@563bf132657a13ded0b01fcb723c5a58cdd824e2 # v7.2.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* global process */
|
||||
// Tasks to generate entry HTML
|
||||
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ const SAFARI_TO_MACOS = {
|
||||
16: [11, 0, 0],
|
||||
17: [12, 0, 0],
|
||||
18: [13, 0, 0],
|
||||
26: [26, 0, 0],
|
||||
};
|
||||
|
||||
const getCommonTemplateVars = () => {
|
||||
|
||||
+25
-18
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
addMinutes,
|
||||
addMonths,
|
||||
differenceInHours,
|
||||
endOfDay,
|
||||
@@ -12,6 +13,19 @@ import type {
|
||||
} from "../../../src/data/recorder";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const getNextDate = (
|
||||
currentDate: Date,
|
||||
period: "5minute" | "hour" | "day" | "month"
|
||||
): Date => {
|
||||
return period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: period === "hour"
|
||||
? addHours(currentDate, 1)
|
||||
: addMinutes(currentDate, 5);
|
||||
};
|
||||
|
||||
const generateMeanStatistics = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
@@ -25,9 +39,10 @@ const generateMeanStatistics = (
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const delta = Math.random() * maxDiff;
|
||||
const mean = delta;
|
||||
const nextDate = getNextDate(currentDate, period);
|
||||
statistics.push({
|
||||
start: currentDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
end: nextDate.getTime(),
|
||||
mean,
|
||||
min: mean - Math.random() * maxDiff,
|
||||
max: mean + Math.random() * maxDiff,
|
||||
@@ -35,12 +50,7 @@ const generateMeanStatistics = (
|
||||
state: mean,
|
||||
sum: null,
|
||||
});
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
currentDate = nextDate;
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
@@ -58,11 +68,12 @@ const generateSumStatistics = (
|
||||
let sum = initValue;
|
||||
const now = new Date();
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const nextDate = getNextDate(currentDate, period);
|
||||
const add = Math.random() * maxDiff;
|
||||
sum += add;
|
||||
statistics.push({
|
||||
start: currentDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
end: nextDate.getTime(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
@@ -71,12 +82,7 @@ const generateSumStatistics = (
|
||||
state: initValue + sum,
|
||||
sum,
|
||||
});
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
currentDate = nextDate;
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
@@ -84,7 +90,7 @@ const generateSumStatistics = (
|
||||
const generateCurvedStatistics = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
_period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number,
|
||||
metered: boolean
|
||||
@@ -98,11 +104,12 @@ const generateCurvedStatistics = (
|
||||
let half = false;
|
||||
const now = new Date();
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const nextDate = getNextDate(currentDate, period);
|
||||
const add = i * (Math.random() * maxDiff);
|
||||
sum += add;
|
||||
statistics.push({
|
||||
start: currentDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
end: nextDate.getTime(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
@@ -111,7 +118,7 @@ const generateCurvedStatistics = (
|
||||
state: initValue + sum,
|
||||
sum: metered ? sum : null,
|
||||
});
|
||||
currentDate = addHours(currentDate, 1);
|
||||
currentDate = nextDate;
|
||||
if (!half && i > hours / 2) {
|
||||
half = true;
|
||||
}
|
||||
@@ -289,7 +296,7 @@ const statisticsFunctions: Record<
|
||||
end,
|
||||
period,
|
||||
productionFinalVal,
|
||||
2
|
||||
0.2
|
||||
);
|
||||
return [...morning, ...production, ...evening, ...rest];
|
||||
},
|
||||
|
||||
+31
-31
@@ -28,26 +28,26 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.2",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.1",
|
||||
"@codemirror/autocomplete": "6.20.2",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/lint": "6.9.5",
|
||||
"@codemirror/lint": "6.9.6",
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.41.1",
|
||||
"@codemirror/view": "6.42.1",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.1",
|
||||
"@formatjs/intl-displaynames": "7.3.4",
|
||||
"@formatjs/intl-durationformat": "0.10.7",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.5",
|
||||
"@formatjs/intl-listformat": "8.3.4",
|
||||
"@formatjs/intl-locale": "5.3.4",
|
||||
"@formatjs/intl-numberformat": "9.3.4",
|
||||
"@formatjs/intl-pluralrules": "6.3.4",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.4",
|
||||
"@formatjs/intl-datetimeformat": "7.4.2",
|
||||
"@formatjs/intl-displaynames": "7.3.5",
|
||||
"@formatjs/intl-durationformat": "0.10.8",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.6",
|
||||
"@formatjs/intl-listformat": "8.3.5",
|
||||
"@formatjs/intl-locale": "5.3.5",
|
||||
"@formatjs/intl-numberformat": "9.3.5",
|
||||
"@formatjs/intl-pluralrules": "6.3.5",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.5",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -80,7 +80,7 @@
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"barcode-detector": "3.1.2",
|
||||
"barcode-detector": "3.1.3",
|
||||
"cally": "0.9.2",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
@@ -99,7 +99,7 @@
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.2.3",
|
||||
"intl-messageformat": "11.2.4",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -121,28 +121,28 @@
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.0",
|
||||
"workbox-core": "7.4.0",
|
||||
"workbox-expiration": "7.4.0",
|
||||
"workbox-precaching": "7.4.0",
|
||||
"workbox-routing": "7.4.0",
|
||||
"workbox-strategies": "7.4.0",
|
||||
"workbox-cacheable-response": "7.4.1",
|
||||
"workbox-core": "7.4.1",
|
||||
"workbox-expiration": "7.4.1",
|
||||
"workbox-precaching": "7.4.1",
|
||||
"workbox-routing": "7.4.1",
|
||||
"workbox-strategies": "7.4.1",
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.8",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.3",
|
||||
"@babel/preset-env": "7.29.5",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.1",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.60.0",
|
||||
"@lokalise/node-api": "15.7.1",
|
||||
"@lokalise/node-api": "16.0.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.9",
|
||||
"@rspack/core": "2.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.10",
|
||||
"@rspack/core": "2.0.2",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -175,7 +175,7 @@
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.4",
|
||||
"fs-extra": "11.3.5",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.6.0",
|
||||
"gulp": "5.0.1",
|
||||
@@ -186,7 +186,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.4.0",
|
||||
"lint-staged": "17.0.3",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
@@ -195,17 +195,17 @@
|
||||
"prettier": "3.8.3",
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "21.1.2",
|
||||
"tar": "7.5.13",
|
||||
"terser-webpack-plugin": "5.5.0",
|
||||
"sinon": "22.0.0",
|
||||
"tar": "7.5.15",
|
||||
"terser-webpack-plugin": "5.6.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.1",
|
||||
"typescript-eslint": "8.59.2",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.5",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.0#~/.yarn/patches/workbox-build-npm-7.4.0-c84561662c.patch"
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
|
||||
},
|
||||
"resolutions": {
|
||||
"lit": "3.3.2",
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Walks up the composed tree (jumping shadow roots → their hosts), returning
|
||||
* the ancestor chain top-down. Used to compare two nodes that may live in
|
||||
* different shadow trees — `Node.compareDocumentPosition` only works within a
|
||||
* single root and returns `DOCUMENT_POSITION_DISCONNECTED` otherwise.
|
||||
*/
|
||||
const composedAncestorPath = (node: Node): Node[] => {
|
||||
const path: Node[] = [];
|
||||
let cur: Node | null = node;
|
||||
while (cur) {
|
||||
path.push(cur);
|
||||
const parent = cur.parentNode;
|
||||
if (parent instanceof ShadowRoot) {
|
||||
cur = parent.host;
|
||||
} else if (parent) {
|
||||
cur = parent;
|
||||
} else {
|
||||
const root = cur.getRootNode();
|
||||
cur = root instanceof ShadowRoot ? root.host : null;
|
||||
}
|
||||
}
|
||||
return path.reverse();
|
||||
};
|
||||
|
||||
/**
|
||||
* Document-order comparator that works across shadow boundaries. Suitable as
|
||||
* the `Array.prototype.sort` callback for collections of nodes that may live
|
||||
* in different shadow trees.
|
||||
*/
|
||||
export const compareNodeOrder = (a: Node, b: Node): number => {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
const pa = composedAncestorPath(a);
|
||||
const pb = composedAncestorPath(b);
|
||||
let i = 0;
|
||||
while (i < pa.length && i < pb.length && pa[i] === pb[i]) {
|
||||
i++;
|
||||
}
|
||||
if (i === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (i === pa.length) {
|
||||
return -1;
|
||||
}
|
||||
if (i === pb.length) {
|
||||
return 1;
|
||||
}
|
||||
// pa[i] and pb[i] are siblings under the LCA, guaranteed same root.
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return pa[i].compareDocumentPosition(pb[i]) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||
? -1
|
||||
: 1;
|
||||
};
|
||||
@@ -1,3 +1,17 @@
|
||||
import {
|
||||
mdiBattery,
|
||||
mdiBattery10,
|
||||
mdiBattery20,
|
||||
mdiBattery30,
|
||||
mdiBattery40,
|
||||
mdiBattery50,
|
||||
mdiBattery60,
|
||||
mdiBattery70,
|
||||
mdiBattery80,
|
||||
mdiBattery90,
|
||||
mdiBatteryAlertVariantOutline,
|
||||
mdiBatteryUnknown,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
const BATTERY_ICONS = {
|
||||
@@ -12,6 +26,18 @@ const BATTERY_ICONS = {
|
||||
90: "mdi:battery-90",
|
||||
100: "mdi:battery",
|
||||
};
|
||||
const BATTERY_ICON_PATHS = {
|
||||
10: mdiBattery10,
|
||||
20: mdiBattery20,
|
||||
30: mdiBattery30,
|
||||
40: mdiBattery40,
|
||||
50: mdiBattery50,
|
||||
60: mdiBattery60,
|
||||
70: mdiBattery70,
|
||||
80: mdiBattery80,
|
||||
90: mdiBattery90,
|
||||
100: mdiBattery,
|
||||
};
|
||||
const BATTERY_CHARGING_ICONS = {
|
||||
10: "mdi:battery-charging-10",
|
||||
20: "mdi:battery-charging-20",
|
||||
@@ -57,3 +83,15 @@ export const batteryLevelIcon = (
|
||||
}
|
||||
return BATTERY_ICONS[batteryRound];
|
||||
};
|
||||
|
||||
export const batteryLevelIconPath = (batteryLevel: number | string): string => {
|
||||
const batteryValue = Number(batteryLevel);
|
||||
if (isNaN(batteryValue)) {
|
||||
return mdiBatteryUnknown;
|
||||
}
|
||||
if (batteryValue <= 5) {
|
||||
return mdiBatteryAlertVariantOutline;
|
||||
}
|
||||
const batteryRound = Math.round(batteryValue / 10) * 10;
|
||||
return BATTERY_ICON_PATHS[batteryRound];
|
||||
};
|
||||
|
||||
@@ -137,7 +137,10 @@ export const computeEntityPickerDisplay = (
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(hass);
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || stateObj.entity_id;
|
||||
const secondary =
|
||||
|
||||
@@ -117,9 +117,6 @@ export const generateEntityFilter = (
|
||||
}
|
||||
}
|
||||
if (entityCategories) {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
const category = entity?.entity_category || "none";
|
||||
if (!entityCategories.has(category)) {
|
||||
return false;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import type { LitElement } from "lit";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, Translation } from "../../types";
|
||||
|
||||
export function computeRTL(hass: HomeAssistant) {
|
||||
const lang = hass.language || "en";
|
||||
if (hass.translationMetadata.translations[lang]) {
|
||||
return hass.translationMetadata.translations[lang].isRTL || false;
|
||||
export function computeRTL(
|
||||
language = "en",
|
||||
translations: Record<string, Translation>
|
||||
) {
|
||||
if (translations[language]) {
|
||||
return translations[language].isRTL || false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function computeRTLDirection(hass: HomeAssistant) {
|
||||
return emitRTLDirection(computeRTL(hass));
|
||||
return emitRTLDirection(
|
||||
computeRTL(hass.language, hass.translationMetadata.translations)
|
||||
);
|
||||
}
|
||||
|
||||
export function emitRTLDirection(rtl: boolean) {
|
||||
|
||||
@@ -1110,7 +1110,7 @@ export class HaChartBase extends LitElement {
|
||||
private _updateSankeyRoam() {
|
||||
const option = this.chart?.getOption();
|
||||
const sankeySeries = (option?.series as any[])?.filter(
|
||||
(s: any) => s.type === "sankey"
|
||||
(s: any) => s != null && s.type === "sankey"
|
||||
);
|
||||
if (sankeySeries?.length) {
|
||||
this.chart?.setOption({
|
||||
|
||||
@@ -293,7 +293,10 @@ export class StateHistoryChartLine extends LitElement {
|
||||
(changedProps.has("hass") &&
|
||||
this._hasEntityStatesChanged(changedProps.get("hass")))
|
||||
) {
|
||||
const rtl = computeRTL(this.hass);
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||
|
||||
@@ -144,7 +144,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
|
||||
const markerLocalized = !computeRTL(this.hass)
|
||||
const markerLocalized = !computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? marker
|
||||
: `<span style="direction: rtl;display:inline-block;margin-right:4px;margin-inline-end:4px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
||||
|
||||
@@ -167,11 +170,12 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
||||
this.isConnected &&
|
||||
(changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES))
|
||||
) {
|
||||
// If the line is more than 5 minutes old, re-gen it
|
||||
// so the X axis grows even if there is no new data
|
||||
@@ -198,7 +202,10 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
? Math.max(this.paddingYAxis, this._yWidth)
|
||||
: 0;
|
||||
const labelMargin = 5;
|
||||
const rtl = computeRTL(this.hass);
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
|
||||
@@ -13,7 +13,9 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import { formatDate } from "../../common/datetime/format_date";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
@@ -241,6 +243,8 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const rendered: Record<string, boolean> = {};
|
||||
const chartIsBar = this.chartType.startsWith("bar");
|
||||
const period = this.period;
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
@@ -252,8 +256,67 @@ export class StatisticsChart extends LitElement {
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
// max series can have 3 values, as the second value is the max-min to form a band
|
||||
const rawValue = String(param.value[2] ?? param.value[1]);
|
||||
let rawValue: string;
|
||||
let rawTime: string;
|
||||
if (chartIsBar) {
|
||||
// For bar charts value is always second value.
|
||||
rawValue = String(param.value[1]);
|
||||
// Time value is third value (un-shifted date) if given, otherwise first value
|
||||
let startTime: Date;
|
||||
let endTime: Date | undefined;
|
||||
if (param.value[2]) {
|
||||
startTime = new Date(param.value[2]);
|
||||
if (param.value[3]) {
|
||||
endTime = new Date(param.value[3]);
|
||||
}
|
||||
} else {
|
||||
startTime = new Date(param.value[0]);
|
||||
}
|
||||
if (
|
||||
period === "year" ||
|
||||
period === "month" ||
|
||||
period === "week" ||
|
||||
period === "day"
|
||||
) {
|
||||
// For year/month/day periods, show only the date
|
||||
rawTime =
|
||||
formatDate(startTime, this.hass.locale, this.hass.config) +
|
||||
(endTime && period !== "day"
|
||||
? ` – ${formatDate(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
} else {
|
||||
// For other time periods, include time in render, and optionally show range
|
||||
// if we have an end time.
|
||||
rawTime =
|
||||
formatDateTimeWithSeconds(
|
||||
startTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) +
|
||||
(endTime
|
||||
? ` – ${formatTimeWithSeconds(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
}
|
||||
} else {
|
||||
// For lines max series can have 3 values, as the second value is the max-min to form a band
|
||||
rawValue = String(param.value[2] ?? param.value[1]);
|
||||
// Time value is always first value
|
||||
rawTime = `${formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)} <br>`;
|
||||
}
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
@@ -265,14 +328,7 @@ export class StatisticsChart extends LitElement {
|
||||
options
|
||||
)}${unit}`;
|
||||
|
||||
const time =
|
||||
index === 0
|
||||
? formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>"
|
||||
: "";
|
||||
const time = index === 0 ? rawTime : "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
@@ -368,7 +424,12 @@ export class StatisticsChart extends LitElement {
|
||||
nameTextStyle: {
|
||||
align: "left",
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
position: computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "right"
|
||||
: "left",
|
||||
scale:
|
||||
this.chartType.startsWith("line") ||
|
||||
this.logarithmicScale ||
|
||||
@@ -506,33 +567,53 @@ export class StatisticsChart extends LitElement {
|
||||
const statDataSets: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
const statLegendData: typeof legendData = [];
|
||||
|
||||
// Place bars at centre of their specified time range if this is a bar chart
|
||||
// and the period is 5minute or hour.
|
||||
const centerBars =
|
||||
chartType === "bar" &&
|
||||
(this.period === "5minute" || this.period === "hour");
|
||||
|
||||
const pushData = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
start: Date, // Data point start time
|
||||
end: Date, // Data point end time
|
||||
limit: Date, // Limit for end time (e.g. now)
|
||||
dataValues: (number | null)[][]
|
||||
) => {
|
||||
if (!dataValues.length) return;
|
||||
if (start > end) {
|
||||
// Limit for time range is lesser of overall limit and data point end
|
||||
limit = end.getTime() < limit.getTime() ? end : limit;
|
||||
if (start.getTime() > limit.getTime()) {
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
}
|
||||
statDataSets.forEach((d, i) => {
|
||||
if (
|
||||
chartType === "line" &&
|
||||
prevEndTime &&
|
||||
prevValues &&
|
||||
prevEndTime.getTime() !== start.getTime()
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
if (chartType === "line") {
|
||||
if (
|
||||
prevEndTime &&
|
||||
prevValues &&
|
||||
prevEndTime.getTime() !== start.getTime()
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
}
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
} else {
|
||||
let time = start;
|
||||
if (centerBars) {
|
||||
// If centering bars, set the time to the midpoint between start and end instead
|
||||
// of the start time.
|
||||
time = new Date((start.getTime() + end.getTime()) / 2);
|
||||
}
|
||||
// Data value should always be a scalar for bar charts. Pass in
|
||||
// real start time as extra value to allow formatting tooltip.
|
||||
d.data!.push([time, dataValues[i][0]!, start, end]);
|
||||
}
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
});
|
||||
prevValues = dataValues;
|
||||
prevEndTime = end;
|
||||
prevEndTime = limit;
|
||||
};
|
||||
|
||||
let color = colors[statistic_id];
|
||||
@@ -692,11 +773,7 @@ export class StatisticsChart extends LitElement {
|
||||
dataValues.push(val);
|
||||
});
|
||||
if (!this._hiddenStats.has(statistic_id)) {
|
||||
pushData(
|
||||
startDate,
|
||||
endDate.getTime() < endTime.getTime() ? endDate : endTime,
|
||||
dataValues
|
||||
);
|
||||
pushData(startDate, endDate, endTime, dataValues);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -127,7 +127,6 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${localize("ui.components.data-table.settings.header")}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -130,7 +130,6 @@ export class HaStateLabelBadge extends LitElement {
|
||||
? html`<ha-state-icon
|
||||
.icon=${this.icon}
|
||||
.stateObj=${entityState}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>`
|
||||
: ""}
|
||||
${value && !image && !showIcon
|
||||
|
||||
@@ -210,7 +210,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(hass);
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const output: StatisticComboBoxItem[] = [];
|
||||
|
||||
@@ -353,7 +356,10 @@ export class HaStatisticPicker extends LitElement {
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || statisticId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
|
||||
@@ -98,7 +98,6 @@ export class StateBadge extends LitElement {
|
||||
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
|
||||
|
||||
return html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
style=${styleMap(this._iconStyle)}
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
@@ -82,8 +81,6 @@ export const ADAPTIVE_DIALOG_MEDIA_QUERY =
|
||||
*/
|
||||
@customElement("ha-adaptive-dialog")
|
||||
export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@@ -202,7 +199,6 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
.ariaLabelledBy=${this._defaultAriaLabelledBy}
|
||||
.ariaDescribedBy=${this.ariaDescribedBy}
|
||||
.flexContent=${this.flexContent}
|
||||
.hass=${this.hass}
|
||||
.open=${this.open}
|
||||
.preventScrimClose=${this.preventScrimClose}
|
||||
>
|
||||
@@ -221,7 +217,6 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this.open}
|
||||
.type=${this.type}
|
||||
.width=${this.width}
|
||||
|
||||
@@ -184,7 +184,10 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
const allEntityIds = Object.values(controlEntities).flat();
|
||||
const uniqueEntityIds = Array.from(new Set(allEntityIds));
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
uniqueEntityIds.forEach((entityId) => {
|
||||
if (isSelected(entityId)) {
|
||||
@@ -261,7 +264,6 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
${item.type === "entity" && item.stateObj
|
||||
? html`<ha-state-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item.stateObj}
|
||||
></ha-state-icon>`
|
||||
: item.domain
|
||||
|
||||
@@ -7,7 +7,6 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||
|
||||
@@ -47,8 +46,6 @@ const SWIPE_LOCKED_CLASSES = new Set(["volume-slider-container", "forecast"]);
|
||||
*/
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@@ -67,6 +64,11 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@state() private _sliderInteractionActive = false;
|
||||
|
||||
// disabled till iOS app fix the "focus_element" implementation
|
||||
// @state()
|
||||
// @consume({ context: configContext, subscribe: true })
|
||||
// private _hassConfig?: ContextType<typeof configContext>;
|
||||
|
||||
@query("#drawer") private _drawer!: HTMLElement;
|
||||
|
||||
@query("#body") private _bodyElement!: HTMLDivElement;
|
||||
@@ -90,13 +92,13 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// disabled till iOS app fix the "focus_element" implementation
|
||||
// if (this.hass && isIosApp(this.hass.auth.external)) {
|
||||
// if (this._hassConfig?.auth.external && isIosApp(this._hassConfig.auth.external)) {
|
||||
// const element = this.renderRoot.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-bottom-sheet-autofocus";
|
||||
// }
|
||||
// this.hass.auth.external?.fireMessage({
|
||||
// this._hassConfig.auth.external.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
|
||||
@@ -39,7 +39,12 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
const items: DisplayItem[] = entities.map((entity) => ({
|
||||
value: entity.entity_id,
|
||||
label: computeStateName(entity),
|
||||
icon: entityIcon(this.hass, entity),
|
||||
icon: entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
entity
|
||||
),
|
||||
}));
|
||||
|
||||
const value: DisplayValue = {
|
||||
|
||||
@@ -59,6 +59,7 @@ export class HaExpansionPanel extends LitElement {
|
||||
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="event"></slot>
|
||||
${!this.leftChevron ? chevronIcon : nothing}
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
|
||||
@@ -122,11 +122,7 @@ export class HaFilterEntities extends LitElement {
|
||||
.selected=${this.value?.includes(entity.entity_id) ?? false}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entity}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon slot="graphic" .stateObj=${entity}></ha-state-icon>
|
||||
${computeStateName(entity)}
|
||||
</ha-check-list-item>`;
|
||||
|
||||
|
||||
@@ -137,7 +137,10 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||
.type=${"areas"}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(this.hass),
|
||||
rtl: computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
),
|
||||
floor: hasFloor,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -166,7 +166,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entity}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -322,7 +321,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${group}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -347,7 +345,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${scene}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -400,7 +397,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${automation}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -452,7 +448,6 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${script}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
|
||||
@@ -63,7 +63,12 @@ export class HaSelectBox extends LitElement {
|
||||
const selected = option.value === this.value;
|
||||
|
||||
const isDark = this.hass?.themes.darkMode || false;
|
||||
const isRTL = this.hass ? computeRTL(this.hass) : false;
|
||||
const isRTL = this.hass
|
||||
? computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
: false;
|
||||
|
||||
const imageSrc =
|
||||
typeof option.image === "object"
|
||||
|
||||
@@ -36,7 +36,15 @@ export class HaIconSelector extends LitElement {
|
||||
const placeholder =
|
||||
this.selector.icon?.placeholder ||
|
||||
stateObj?.attributes.icon ||
|
||||
(stateObj && until(entityIcon(this.hass, stateObj)));
|
||||
(stateObj &&
|
||||
until(
|
||||
entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
stateObj
|
||||
)
|
||||
));
|
||||
|
||||
return html`
|
||||
<ha-icon-picker
|
||||
@@ -51,11 +59,7 @@ export class HaIconSelector extends LitElement {
|
||||
>
|
||||
${!placeholder && stateObj
|
||||
? html`
|
||||
<ha-state-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon slot="start" .stateObj=${stateObj}></ha-state-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-icon-picker>
|
||||
|
||||
@@ -523,7 +523,10 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
private _renderUserItem(selectedPanel: string) {
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const isSelected = selectedPanel === "profile";
|
||||
|
||||
return html`
|
||||
@@ -561,9 +564,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
id="sidebar-external-config"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.sidebar.external_app_configuration")}</span
|
||||
>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</ha-list-item-button>
|
||||
${!this.alwaysExpand
|
||||
? this._renderToolTip(
|
||||
@@ -740,6 +743,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--ha-row-item-min-height: var(--ha-space-10);
|
||||
--ha-row-item-padding-block: 0;
|
||||
--ha-row-item-padding-inline: var(--ha-space-3);
|
||||
width: var(--ha-space-12);
|
||||
position: relative;
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
@@ -840,21 +844,12 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
width: var(--ha-space-10);
|
||||
height: var(--ha-space-10);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
ha-list-item-button.user {
|
||||
--ha-row-item-padding-inline: var(--ha-space-2) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-list-item-button.user.rtl {
|
||||
--ha-row-item-padding-inline: var(--ha-space-4) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
flex-shrink: 0;
|
||||
margin-right: calc(var(--ha-space-2) * -1);
|
||||
--ha-row-item-padding-inline: var(--ha-space-1) 0;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
|
||||
@@ -1,31 +1,46 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
entitiesContext,
|
||||
} from "../data/context";
|
||||
import {
|
||||
DEFAULT_DOMAIN_ICON,
|
||||
entityIcon,
|
||||
FALLBACK_DOMAIN_ICONS,
|
||||
} from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-state-icon")
|
||||
export class HaStateIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property({ attribute: false }) public stateValue?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
protected _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
protected _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
protected _entities?: ContextType<typeof entitiesContext>;
|
||||
|
||||
protected render() {
|
||||
const overrideIcon =
|
||||
this.icon ||
|
||||
(this.stateObj && this.hass?.entities[this.stateObj.entity_id]?.icon) ||
|
||||
(this.stateObj && this._entities?.[this.stateObj.entity_id]?.icon) ||
|
||||
this.stateObj?.attributes.icon;
|
||||
if (overrideIcon) {
|
||||
return html`<ha-icon .icon=${overrideIcon}></ha-icon>`;
|
||||
@@ -33,17 +48,21 @@ export class HaStateIcon extends LitElement {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
if (!this.hass) {
|
||||
if (!this._config || !this._connection || !this._entities) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
const icon = entityIcon(this.hass, this.stateObj, this.stateValue).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
const icon = entityIcon(
|
||||
this._entities,
|
||||
this._config.config,
|
||||
this._connection.connection,
|
||||
this.stateObj,
|
||||
this.stateValue
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
);
|
||||
return this._renderFallback();
|
||||
});
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -228,6 +228,10 @@ export class HaSwitch extends Switch {
|
||||
outline: var(--wa-focus-ring);
|
||||
outline-offset: var(--wa-focus-ring-offset);
|
||||
}
|
||||
|
||||
:host(:empty) slot.label {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1136,7 +1136,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
let rtl = false;
|
||||
let showEntityId = false;
|
||||
if (type === "area" || type === "floor") {
|
||||
rtl = computeRTL(this.hass);
|
||||
rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
hasFloor =
|
||||
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../list/types";
|
||||
import { HaRowItem } from "./ha-row-item";
|
||||
|
||||
/**
|
||||
@@ -39,6 +41,12 @@ export class HaListItemBase extends HaRowItem {
|
||||
if (!this.hasAttribute("role")) {
|
||||
this.setAttribute("role", this.defaultRole);
|
||||
}
|
||||
fireEvent(this, "ha-list-item-register", { item: this });
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
fireEvent(this, "ha-list-item-unregister", { item: this });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* @element ha-row-item
|
||||
@@ -46,13 +46,34 @@ export class HaRowItem extends LitElement {
|
||||
|
||||
protected readonly _slotController = new HasSlotController(
|
||||
this,
|
||||
"start",
|
||||
"end",
|
||||
"headline",
|
||||
"supporting-text",
|
||||
"content"
|
||||
);
|
||||
|
||||
@state() private _hasStart = false;
|
||||
|
||||
@state() private _hasEnd = false;
|
||||
|
||||
private _onSlotChange(name: "start" | "end") {
|
||||
return (ev: Event) => {
|
||||
const slot = ev.target as HTMLSlotElement;
|
||||
const hasContent = slot
|
||||
.assignedNodes({ flatten: true })
|
||||
.some(
|
||||
(node) =>
|
||||
node.nodeType === Node.ELEMENT_NODE ||
|
||||
(node.nodeType === Node.TEXT_NODE &&
|
||||
(node as Text).textContent?.trim() !== "")
|
||||
);
|
||||
if (name === "start") {
|
||||
this._hasStart = hasContent;
|
||||
} else {
|
||||
this._hasEnd = hasContent;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this._renderBase(this._renderInner());
|
||||
}
|
||||
@@ -62,26 +83,20 @@ export class HaRowItem extends LitElement {
|
||||
}
|
||||
|
||||
protected _renderInner(): TemplateResult {
|
||||
const hasStart = this._slotController.test("start");
|
||||
const hasEnd = this._slotController.test("end");
|
||||
const hasContent = this._slotController.test("content");
|
||||
|
||||
return html`
|
||||
${hasStart
|
||||
? html`<div part="start" class="start">
|
||||
<slot name="start"></slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div part="start" class="start" ?hidden=${!this._hasStart}>
|
||||
<slot name="start" @slotchange=${this._onSlotChange("start")}></slot>
|
||||
</div>
|
||||
<div part="content" class="content">
|
||||
${hasContent
|
||||
? html`<slot name="content"></slot>`
|
||||
: this._renderDefaultContent()}
|
||||
</div>
|
||||
${hasEnd
|
||||
? html`<div part="end" class="end">
|
||||
<slot name="end"></slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div part="end" class="end" ?hidden=${!this._hasEnd}>
|
||||
<slot name="end" @slotchange=${this._onSlotChange("end")}></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -148,6 +163,10 @@ export class HaRowItem extends LitElement {
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.start[hidden],
|
||||
.end[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.headline {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import { compareNodeOrder } from "../../common/dom/compare-node-order";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import "./types";
|
||||
import type { HaListItemRegistrationDetail } from "./types";
|
||||
|
||||
/**
|
||||
* @element ha-list-base
|
||||
@@ -12,9 +14,11 @@ import "./types";
|
||||
*
|
||||
* @summary
|
||||
* Base list container with roving-tabindex keyboard navigation (ArrowUp/Down,
|
||||
* Home/End, optional Enter/Space activation, optional wrap-focus). Discovers
|
||||
* slotted `HaListItemBase` descendants. Subclasses override `hostRole` and/or
|
||||
* `render()` to specialize.
|
||||
* Home/End, optional Enter/Space activation, optional wrap-focus). Tracks
|
||||
* `HaListItemBase` descendants via the `ha-list-item-register` /
|
||||
* `ha-list-item-unregister` events they fire on connect/disconnect — works
|
||||
* across any nesting depth and shadow boundaries. Subclasses override
|
||||
* `hostRole` and/or `render()` to specialize.
|
||||
*
|
||||
* @slot - List items (`<ha-list-item-*>`).
|
||||
*
|
||||
@@ -68,6 +72,14 @@ export class HaListBase extends LitElement {
|
||||
Space: this._onActivate,
|
||||
});
|
||||
this.addEventListener("focusin", this._onFocusIn);
|
||||
this.addEventListener(
|
||||
"ha-list-item-register",
|
||||
this._onItemRegister as EventListener
|
||||
);
|
||||
this.addEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this._onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
@@ -75,11 +87,14 @@ export class HaListBase extends LitElement {
|
||||
this._unbindKeys?.();
|
||||
this._unbindKeys = undefined;
|
||||
this.removeEventListener("focusin", this._onFocusIn);
|
||||
}
|
||||
|
||||
public firstUpdated(changed: PropertyValues) {
|
||||
super.firstUpdated(changed);
|
||||
this.updateListItems();
|
||||
this.removeEventListener(
|
||||
"ha-list-item-register",
|
||||
this._onItemRegister as EventListener
|
||||
);
|
||||
this.removeEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this._onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
public focus(options?: FocusOptions) {
|
||||
@@ -115,18 +130,14 @@ export class HaListBase extends LitElement {
|
||||
this._applyActive(focusItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called whenever the items array has changed. Subclasses can override
|
||||
* to layer in extra bookkeeping (e.g. selection state sync).
|
||||
*/
|
||||
public updateListItems() {
|
||||
const next = this._discoverListItems();
|
||||
const changed =
|
||||
next.length !== this.items.length ||
|
||||
next.some((it, i) => it !== this.items[i]);
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
this.items = next;
|
||||
this._recomputeFocusableIndexes();
|
||||
if (
|
||||
this._activeItemIndex >= next.length ||
|
||||
this._activeItemIndex >= this.items.length ||
|
||||
!this._hasFocusableItem ||
|
||||
this._activeItemIndex < 0
|
||||
) {
|
||||
@@ -135,6 +146,32 @@ export class HaListBase extends LitElement {
|
||||
this._applyActive(false);
|
||||
}
|
||||
|
||||
private _onItemRegister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.detail.item;
|
||||
if (this.items.includes(item)) {
|
||||
return;
|
||||
}
|
||||
const next = [...this.items, item];
|
||||
next.sort(compareNodeOrder);
|
||||
this.items = next;
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
private _onItemUnregister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.detail.item;
|
||||
if (!this.items.includes(item)) {
|
||||
return;
|
||||
}
|
||||
this.items = this.items.filter((it) => it !== item);
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
private _recomputeFocusableIndexes() {
|
||||
let first = -1;
|
||||
let last = -1;
|
||||
@@ -151,27 +188,12 @@ export class HaListBase extends LitElement {
|
||||
this._hasFocusableItem = first !== -1;
|
||||
}
|
||||
|
||||
public handleSlotChange = () => {
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<div part="base" class="base">
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _discoverListItems(): HaListItemBase[] {
|
||||
const slot =
|
||||
this.renderRoot?.querySelector<HTMLSlotElement>("slot:not([name])");
|
||||
if (!slot) {
|
||||
return [];
|
||||
}
|
||||
return slot
|
||||
.assignedElements({ flatten: true })
|
||||
.filter((el): el is HaListItemBase => el instanceof HaListItemBase);
|
||||
}
|
||||
|
||||
private _isFocusable(index: number): boolean {
|
||||
const item = this.items[index];
|
||||
return !!item && item.interactive && !item.disabled;
|
||||
|
||||
@@ -31,7 +31,7 @@ export class HaListNav extends HaListBase {
|
||||
aria-label=${ifDefined(this.ariaLabel ?? undefined)}
|
||||
>
|
||||
<div part="base" class="base" role="list">
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</nav>`;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,15 @@ export interface HaListActivatedDetail {
|
||||
item: HaListItemBase;
|
||||
}
|
||||
|
||||
export interface HaListItemRegistrationDetail {
|
||||
item: HaListItemBase;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"ha-list-selected": HaListSelectedDetail;
|
||||
"ha-list-activated": HaListActivatedDetail;
|
||||
"ha-list-item-register": HaListItemRegistrationDetail;
|
||||
"ha-list-item-unregister": HaListItemRegistrationDetail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ class HaEntityMarker extends LitElement {
|
||||
></div>`
|
||||
: this.showIcon && this.entityId
|
||||
? html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass?.states[this.entityId]}
|
||||
></ha-state-icon>`
|
||||
: !this.entityUnit
|
||||
|
||||
@@ -76,12 +76,7 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
|
||||
const entityId = this._entityId;
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog .open=${this._open} flexcontent @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
|
||||
@@ -100,7 +100,6 @@ class DialogMediaManage extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
?prevent-scrim-close=${this._uploading || this._deleting}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -77,7 +77,6 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="large"
|
||||
flexcontent
|
||||
|
||||
@@ -59,7 +59,10 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
icon = mdiSpeakerPause;
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../../data/device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity/entity";
|
||||
import type { EntitySources } from "../../../data/entity/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
|
||||
import type { TargetSelector } from "../../../data/selector";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
} from "../../../data/selector";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
|
||||
import "../../ha-dialog";
|
||||
import "../../ha-dialog-header";
|
||||
import "../../ha-icon-button";
|
||||
@@ -10,6 +25,7 @@ import "../../ha-icon-next";
|
||||
import "../../ha-md-list";
|
||||
import "../../ha-md-list-item";
|
||||
import "../../ha-svg-icon";
|
||||
import "../../list/ha-list-base";
|
||||
import "../ha-target-picker-item-row";
|
||||
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
|
||||
|
||||
@@ -21,6 +37,12 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _entitySourcesLoaded = false;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public showDialog(params: TargetDetailsDialogParams): void {
|
||||
this._params = params;
|
||||
this._opened = true;
|
||||
@@ -34,6 +56,72 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
private _dialogClosed() {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
this._params = undefined;
|
||||
this._entitySources = undefined;
|
||||
this._entitySourcesLoaded = false;
|
||||
}
|
||||
|
||||
private _hasIntegration(selector: TargetSelector) {
|
||||
return (
|
||||
(selector.target?.entity &&
|
||||
ensureArray(selector.target.entity).some((e) => e.integration)) ||
|
||||
(selector.target?.device &&
|
||||
ensureArray(selector.target.device).some((d) => d.integration))
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (!changedProperties.has("_params")) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this._params?.selector &&
|
||||
this._hasIntegration(this._params.selector) &&
|
||||
!this._entitySourcesLoaded
|
||||
) {
|
||||
this._loadEntitySources();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadEntitySources(): Promise<void> {
|
||||
try {
|
||||
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load entity sources for target details", err);
|
||||
} finally {
|
||||
this._entitySourcesLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
const target = this._selectorTarget();
|
||||
if (!target?.entity) {
|
||||
return true;
|
||||
}
|
||||
return ensureArray(target.entity).some((e) =>
|
||||
filterSelectorEntities(e, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
const target = this._selectorTarget();
|
||||
if (!target?.device) {
|
||||
return true;
|
||||
}
|
||||
const deviceIntegrations = this._entitySources
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
Object.values(this.hass.entities)
|
||||
)
|
||||
: undefined;
|
||||
return ensureArray(target.device).some((d) =>
|
||||
filterSelectorDevices(d, device, deviceIntegrations)
|
||||
);
|
||||
};
|
||||
|
||||
private _selectorTarget() {
|
||||
return this._params?.selector?.target || null;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -41,33 +129,86 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let deviceFilter: HaDevicePickerDeviceFilterFunc | undefined;
|
||||
let entityFilter: HaEntityPickerEntityFilterFunc | undefined;
|
||||
let includeDomains: string[] | undefined;
|
||||
let includeDeviceClasses: string[] | undefined;
|
||||
let primaryEntitiesOnly: boolean | undefined;
|
||||
|
||||
if (this._params.selector) {
|
||||
deviceFilter = this._filterDevices;
|
||||
entityFilter = this._filterEntities;
|
||||
primaryEntitiesOnly = this._params.selector.target?.primary_entities_only;
|
||||
} else {
|
||||
deviceFilter = this._params.deviceFilter;
|
||||
entityFilter = this._params.entityFilter;
|
||||
includeDomains = this._params.includeDomains;
|
||||
includeDeviceClasses = this._params.includeDeviceClasses;
|
||||
primaryEntitiesOnly = this._params.primaryEntitiesOnly;
|
||||
}
|
||||
|
||||
const waitingForSources =
|
||||
this._params.selector &&
|
||||
this._hasIntegration(this._params.selector) &&
|
||||
!this._entitySourcesLoaded;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._opened}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.target-picker.target_details"
|
||||
)}
|
||||
header-subtitle=${`${this.hass.localize(
|
||||
`ui.components.target-picker.type.${this._params.type}`
|
||||
)}:
|
||||
${this._params.title}`}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${this._params.type}
|
||||
.itemId=${this._params.itemId}
|
||||
.deviceFilter=${this._params.deviceFilter}
|
||||
.entityFilter=${this._params.entityFilter}
|
||||
.includeDomains=${this._params.includeDomains}
|
||||
.includeDeviceClasses=${this._params.includeDeviceClasses}
|
||||
.primaryEntitiesOnly=${this._params.primaryEntitiesOnly}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
<div class="type-wrapper">
|
||||
<div class="type-label">
|
||||
${this.hass.localize(
|
||||
`ui.components.target-picker.type.${this._params.type}`
|
||||
)}
|
||||
</div>
|
||||
<ha-list-base
|
||||
.ariaLabel=${`${this.hass.localize(`ui.components.target-picker.type.${this._params.type}`)}: ${this._params.title}`}
|
||||
wrap-focus
|
||||
>
|
||||
${waitingForSources
|
||||
? nothing
|
||||
: html`
|
||||
<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${this._params.type}
|
||||
.itemId=${this._params.itemId}
|
||||
.deviceFilter=${deviceFilter}
|
||||
.entityFilter=${entityFilter}
|
||||
.includeDomains=${includeDomains}
|
||||
.includeDeviceClasses=${includeDeviceClasses}
|
||||
.primaryEntitiesOnly=${primaryEntitiesOnly}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`}
|
||||
</ha-list-base>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.type-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
border: var(--ha-border-width-sm) solid
|
||||
var(--ha-color-border-neutral-normal);
|
||||
overflow: hidden;
|
||||
}
|
||||
.type-label {
|
||||
background-color: var(--ha-color-surface-low);
|
||||
padding: var(--ha-space-1) var(--ha-space-3);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity/entity";
|
||||
import type { TargetSelector } from "../../../data/selector";
|
||||
import type { TargetType } from "../../../data/target";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
|
||||
|
||||
export type NewBackupType = "automatic" | "manual";
|
||||
|
||||
export interface TargetDetailsDialogParams {
|
||||
title: string;
|
||||
type: TargetType;
|
||||
itemId: string;
|
||||
selector?: TargetSelector;
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
includeDomains?: string[];
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { TargetType, TargetTypeFloorless } from "../../data/target";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
|
||||
import "../ha-expansion-panel";
|
||||
import "../ha-md-list";
|
||||
import "../list/ha-list-base";
|
||||
import "./ha-target-picker-item-row";
|
||||
|
||||
@customElement("ha-target-picker-item-group")
|
||||
@@ -66,23 +66,25 @@ export class HaTargetPickerItemGroup extends LitElement {
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
${Object.entries(this.items).map(([type, items]) =>
|
||||
items
|
||||
? items.map(
|
||||
(item) =>
|
||||
html`<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${type as TargetTypeFloorless}
|
||||
.itemId=${item}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.primaryEntitiesOnly=${this.primaryEntitiesOnly}
|
||||
></ha-target-picker-item-row>`
|
||||
)
|
||||
: nothing
|
||||
)}
|
||||
<ha-list-base>
|
||||
${Object.entries(this.items).map(([type, items]) =>
|
||||
items
|
||||
? items.map(
|
||||
(item) =>
|
||||
html`<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${type as TargetTypeFloorless}
|
||||
.itemId=${item}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.primaryEntitiesOnly=${this.primaryEntitiesOnly}
|
||||
></ha-target-picker-item-row>`
|
||||
)
|
||||
: nothing
|
||||
)}
|
||||
</ha-list-base>
|
||||
</ha-expansion-panel>`;
|
||||
}
|
||||
|
||||
@@ -96,7 +98,7 @@ export class HaTargetPickerItemGroup extends LitElement {
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
ha-expansion-panel::part(summary) {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
background-color: var(--ha-color-surface-low);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
@@ -104,9 +106,6 @@ export class HaTargetPickerItemGroup extends LitElement {
|
||||
justify-content: space-between;
|
||||
min-height: unset;
|
||||
}
|
||||
ha-md-list {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiClose,
|
||||
mdiDevices,
|
||||
mdiHome,
|
||||
mdiLabel,
|
||||
mdiMinusBox,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -38,18 +48,17 @@ import {
|
||||
type ExtractFromTargetResultReferenced,
|
||||
type TargetType,
|
||||
} from "../../data/target";
|
||||
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { buttonLinkStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
|
||||
import { floorDefaultIconPath } from "../ha-floor-icon";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-md-list";
|
||||
import type { HaMdList } from "../ha-md-list";
|
||||
import "../ha-md-list-item";
|
||||
import type { HaMdListItem } from "../ha-md-list-item";
|
||||
import "../ha-state-icon";
|
||||
import "../ha-svg-icon";
|
||||
import "../item/ha-list-item-base";
|
||||
import "../item/ha-list-item-button";
|
||||
import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details";
|
||||
|
||||
@customElement("ha-target-picker-item-row")
|
||||
@@ -65,6 +74,9 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
@property({ type: Boolean, attribute: "sub-entry", reflect: true })
|
||||
public subEntry = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public subLevel = 0;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-context" })
|
||||
public hideContext = false;
|
||||
|
||||
@@ -106,12 +118,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelRegistry!: LabelRegistryEntry[];
|
||||
|
||||
@query("ha-md-list-item") public item?: HaMdListItem;
|
||||
|
||||
@query("ha-md-list") public list?: HaMdList;
|
||||
|
||||
@query("ha-target-picker-item-row") public itemRow?: HaTargetPickerItemRow;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (!this.subEntry && changedProps.has("itemId")) {
|
||||
this._updateItemData();
|
||||
@@ -137,101 +143,130 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
const replaceable = !this.subEntry && !this.expand;
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
type=${replaceable ? "button" : "text"}
|
||||
class=${classMap({
|
||||
error: notFound,
|
||||
replaceable,
|
||||
})}
|
||||
@click=${replaceable ? this._replaceItem : undefined}
|
||||
>
|
||||
<div class="icon" slot="start">
|
||||
${this.subEntry
|
||||
? html`
|
||||
<div class="horizontal-line-wrapper">
|
||||
<div class="horizontal-line"></div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${iconPath
|
||||
? html`<ha-icon .icon=${iconPath}></ha-icon>`
|
||||
: this._iconImg
|
||||
? html`<img
|
||||
alt=${this._domainName || ""}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${this._iconImg}
|
||||
/>`
|
||||
: fallbackIconPath
|
||||
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||
: this.type === "entity"
|
||||
? html`
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObject ||
|
||||
({
|
||||
entity_id: this.itemId,
|
||||
attributes: {},
|
||||
} as HassEntity)}
|
||||
>
|
||||
</ha-state-icon>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
const content = html`
|
||||
<div class="icon" slot="start">
|
||||
${iconPath
|
||||
? html`<ha-icon .icon=${iconPath}></ha-icon>`
|
||||
: this._iconImg
|
||||
? html`<img
|
||||
alt=${this._domainName || ""}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${this._iconImg}
|
||||
/>`
|
||||
: fallbackIconPath
|
||||
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||
: this.type === "entity"
|
||||
? html`
|
||||
<ha-state-icon
|
||||
.stateObj=${stateObject ||
|
||||
({
|
||||
entity_id: this.itemId,
|
||||
attributes: {},
|
||||
} as HassEntity)}
|
||||
>
|
||||
</ha-state-icon>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<div slot="headline">${name}</div>
|
||||
${notFound || (context && !this.hideContext)
|
||||
? html`<span slot="supporting-text"
|
||||
>${notFound
|
||||
? this.hass.localize(
|
||||
`ui.components.target-picker.${this.type}_not_found`
|
||||
)
|
||||
: context}</span
|
||||
>`
|
||||
: nothing}
|
||||
${this._domainName && this.subEntry
|
||||
? html`<span slot="supporting-text" class="domain"
|
||||
>${this._domainName}</span
|
||||
>`
|
||||
: nothing}
|
||||
${!this.subEntry && entries && showEntities
|
||||
? html`
|
||||
<div slot="end" class="summary">
|
||||
${showEntities &&
|
||||
!this.expand &&
|
||||
entries?.referenced_entities.length
|
||||
? html`<button class="main link" @click=${this._openDetails}>
|
||||
<div slot="headline">${name}</div>
|
||||
${notFound || (context && !this.hideContext)
|
||||
? html`<span slot="supporting-text"
|
||||
>${notFound
|
||||
? this.hass.localize(
|
||||
`ui.components.target-picker.${this.type}_not_found`
|
||||
)
|
||||
: context}</span
|
||||
>`
|
||||
: nothing}
|
||||
${stateObject && this.subEntry
|
||||
? html`<span slot="supporting-text" class="state"
|
||||
>${this.hass.formatEntityState(stateObject)}</span
|
||||
>`
|
||||
: nothing}
|
||||
${!this.subEntry && entries && showEntities
|
||||
? html`
|
||||
<div slot="end" class="summary">
|
||||
${showEntities &&
|
||||
!this.expand &&
|
||||
entries?.referenced_entities.length
|
||||
? html`<button class="main link" @click=${this._openDetails}>
|
||||
${this.hass.localize(
|
||||
"ui.components.target-picker.entities_count",
|
||||
{
|
||||
count: entries?.referenced_entities.length,
|
||||
}
|
||||
)}
|
||||
</button>`
|
||||
: showEntities
|
||||
? html`<span class="main">
|
||||
${this.hass.localize(
|
||||
"ui.components.target-picker.entities_count",
|
||||
{
|
||||
count: entries?.referenced_entities.length,
|
||||
}
|
||||
)}
|
||||
</button>`
|
||||
: showEntities
|
||||
? html`<span class="main">
|
||||
${this.hass.localize(
|
||||
"ui.components.target-picker.entities_count",
|
||||
{
|
||||
count: entries?.referenced_entities.length,
|
||||
}
|
||||
)}
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${!this.expand && !this.subEntry
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${!this.expand && !this.subEntry
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
slot="end"
|
||||
@click=${this._removeItem}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: this.subEntry && this.type === "entity"
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
<ha-svg-icon
|
||||
.path=${computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? mdiChevronLeft
|
||||
: mdiChevronRight}
|
||||
slot="end"
|
||||
@click=${this._removeItem}
|
||||
></ha-icon-button>
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
|
||||
let item: TemplateResult;
|
||||
|
||||
if (replaceable || (this.subEntry && this.type === "entity")) {
|
||||
item = html`
|
||||
<ha-list-item-button
|
||||
class=${classMap({
|
||||
error: notFound,
|
||||
replaceable,
|
||||
})}
|
||||
@click=${replaceable
|
||||
? this._replaceItem
|
||||
: this.subEntry && this.type === "entity"
|
||||
? this._openMoreInfo
|
||||
: undefined}
|
||||
>
|
||||
${content}
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
} else {
|
||||
item = html`
|
||||
<ha-list-item-base
|
||||
class=${classMap({
|
||||
error: notFound,
|
||||
})}
|
||||
>
|
||||
${content}
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${item}
|
||||
${this.expand && entries && entries.referenced_entities
|
||||
? this._renderEntries()
|
||||
: nothing}
|
||||
@@ -241,6 +276,10 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
private _renderEntries() {
|
||||
const entries = this.parentEntries || this._entries;
|
||||
|
||||
if (!entries || entries.referenced_entities.length === 0) {
|
||||
return this._renderEmptyEntries();
|
||||
}
|
||||
|
||||
let nextType: TargetType =
|
||||
this.type === "floor"
|
||||
? "area"
|
||||
@@ -350,54 +389,64 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
) || ([] as string[]),
|
||||
}));
|
||||
|
||||
const nextSubLevel = this.subLevel + 1;
|
||||
|
||||
return html`
|
||||
<div class="entries-tree">
|
||||
<div class="line-wrapper">
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<ha-md-list class="entries">
|
||||
${rows1.map(
|
||||
(itemId, index) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.hass=${this.hass}
|
||||
.type=${nextType}
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${rows1Entries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
${deviceRows.map(
|
||||
(itemId, index) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.hass=${this.hass}
|
||||
type="device"
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${deviceRowsEntries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
${entityRows.map(
|
||||
(itemId) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.hass=${this.hass}
|
||||
type="entity"
|
||||
.itemId=${itemId}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</div>
|
||||
${rows1.map(
|
||||
(itemId, index) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.subLevel=${nextSubLevel}
|
||||
style=${`--sub-entry-indent: calc(${nextSubLevel} * var(--ha-space-10));`}
|
||||
.hass=${this.hass}
|
||||
.type=${nextType}
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${rows1Entries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
${deviceRows.map(
|
||||
(itemId, index) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.subLevel=${nextSubLevel}
|
||||
style=${`--sub-entry-indent: calc(${nextSubLevel} * var(--ha-space-10));`}
|
||||
.hass=${this.hass}
|
||||
type="device"
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${deviceRowsEntries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
${entityRows.map(
|
||||
(itemId) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.subLevel=${nextSubLevel}
|
||||
style=${`--sub-entry-indent: calc(${nextSubLevel} * var(--ha-space-10));`}
|
||||
.hass=${this.hass}
|
||||
type="entity"
|
||||
.itemId=${itemId}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEmptyEntries() {
|
||||
return html`<ha-list-item-base>
|
||||
<ha-svg-icon .path=${mdiMinusBox} slot="start" class="icon"></ha-svg-icon>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize("ui.components.target-picker.no_targets")}</span
|
||||
>
|
||||
</ha-list-item-base>`;
|
||||
}
|
||||
|
||||
private async _updateItemData() {
|
||||
if (this.type === "entity") {
|
||||
this._entries = undefined;
|
||||
@@ -566,7 +615,14 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const context = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
|
||||
.join(
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? " ◂ "
|
||||
: " ▸ "
|
||||
);
|
||||
return {
|
||||
name: entityName || deviceName || item,
|
||||
context,
|
||||
@@ -640,6 +696,12 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _openMoreInfo = () => {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this.itemId,
|
||||
});
|
||||
};
|
||||
|
||||
static styles = [
|
||||
buttonLinkStyle,
|
||||
css`
|
||||
@@ -651,12 +713,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
--md-list-item-two-line-container-height: 56px;
|
||||
}
|
||||
|
||||
:host([expand]:not([sub-entry])) ha-md-list-item {
|
||||
border: 2px solid var(--ha-color-border-neutral-loud);
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
|
||||
.error {
|
||||
background: var(--ha-color-fill-warning-quiet-resting);
|
||||
}
|
||||
@@ -680,6 +736,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
.icon {
|
||||
width: 24px;
|
||||
display: flex;
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -697,53 +754,21 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
:host([sub-entry]) .summary {
|
||||
margin-right: var(--ha-space-12);
|
||||
margin-inline-start: var(--ha-space-12);
|
||||
}
|
||||
.summary .main {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
:host([expand]) .summary .main {
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
.summary .secondary {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.entries-tree {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.entries-tree .line-wrapper {
|
||||
padding: var(--ha-space-5);
|
||||
}
|
||||
|
||||
.entries-tree .line-wrapper .line {
|
||||
border-left: 2px dashed var(--divider-color);
|
||||
height: calc(100% - 28px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
:host([sub-entry]) .entries-tree .line-wrapper .line {
|
||||
height: calc(100% - 12px);
|
||||
top: -18px;
|
||||
}
|
||||
|
||||
.entries {
|
||||
padding: 0;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
|
||||
.horizontal-line-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.horizontal-line-wrapper .horizontal-line {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
margin-inline-start: -28px;
|
||||
width: 29px;
|
||||
border-top: 2px dashed var(--divider-color);
|
||||
}
|
||||
|
||||
button.link {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
@@ -754,12 +779,19 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.domain {
|
||||
.state {
|
||||
width: fit-content;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1);
|
||||
font-family: var(--ha-font-family-code);
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
ha-list-item-button::part(end) {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
:host([sub-entry]) ha-list-item-button::part(base),
|
||||
:host([sub-entry]) ha-list-item-base::part(base) {
|
||||
padding-inline-start: var(--sub-entry-indent);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -76,7 +76,6 @@ export class HaTargetPickerValueChip extends LitElement {
|
||||
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||
: stateObject
|
||||
? html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObject}
|
||||
></ha-state-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -99,7 +99,8 @@ export class HaTileContainer extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
padding: 0 10px;
|
||||
min-height: var(--row-height, 56px);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import type { Trigger } from "../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../data/automation";
|
||||
@@ -23,9 +22,10 @@ import "../../panels/logbook/ha-logbook-renderer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-tab-group";
|
||||
import "../ha-tab-group-tab";
|
||||
import "./hat-logbook-note";
|
||||
import type { NodeInfo } from "./hat-script-graph";
|
||||
import { traceTabStyles } from "./trace-tab-styles";
|
||||
|
||||
const TRACE_PATH_TABS = [
|
||||
"step_config",
|
||||
@@ -66,21 +66,21 @@ export class HaTracePathDetails extends LitElement {
|
||||
${this._renderSelectedTraceInfo()}
|
||||
</div>
|
||||
|
||||
<div class="tabs top">
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
${TRACE_PATH_TABS.map(
|
||||
(view) => html`
|
||||
<button
|
||||
.view=${view}
|
||||
class=${classMap({ active: this._view === view })}
|
||||
@click=${this._showTab}
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${view}`
|
||||
)}
|
||||
</button>
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-tab-group>
|
||||
${this._view === "step_config"
|
||||
? this._renderSelectedConfig()
|
||||
: this._view === "changed_variables"
|
||||
@@ -308,7 +308,12 @@ export class HaTracePathDetails extends LitElement {
|
||||
? this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.no_variables_changed"
|
||||
)
|
||||
: html`<pre>${dump(trace.changed_variables).trimEnd()}</pre>`}
|
||||
: html`<ha-code-editor
|
||||
read-only
|
||||
dir="ltr"
|
||||
.hass=${this.hass}
|
||||
.value=${dump(trace.changed_variables).trimEnd()}
|
||||
></ha-code-editor>`}
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -383,13 +388,12 @@ export class HaTracePathDetails extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _showTab(ev) {
|
||||
this._view = ev.target.view;
|
||||
private _handleTabChanged(ev: CustomEvent) {
|
||||
this._view = ev.detail.name as typeof this._view;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
traceTabStyles,
|
||||
css`
|
||||
.padded-box {
|
||||
margin: 16px;
|
||||
@@ -406,6 +410,16 @@ export class HaTracePathDetails extends LitElement {
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
ha-tab-group {
|
||||
background-color: var(--primary-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { LogbookEntry } from "../../data/logbook";
|
||||
import { localizeTriggerDescription } from "../../data/logbook";
|
||||
import type {
|
||||
ChooseAction,
|
||||
IfAction,
|
||||
@@ -332,7 +333,10 @@ class ActionRenderer {
|
||||
: "other",
|
||||
alias: triggerStep.changed_variables.trigger?.alias,
|
||||
triggeredPath: triggerStep.path === "trigger" ? "manual" : "trigger",
|
||||
trigger: this.trace.trigger,
|
||||
trigger: localizeTriggerDescription(
|
||||
this.hass.localize,
|
||||
this.trace.trigger
|
||||
),
|
||||
time: formatDateTimeWithSeconds(
|
||||
new Date(triggerStep.timestamp),
|
||||
this.hass.locale,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const traceTabStyles = css`
|
||||
.tabs {
|
||||
background-color: var(--primary-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
padding-left: 4px;
|
||||
padding-inline-start: 4px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
|
||||
.tabs.top {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tabs > * {
|
||||
padding: 2px 16px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
bottom: -1px;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
user-select: none;
|
||||
background: none;
|
||||
color: var(--primary-text-color);
|
||||
outline: none;
|
||||
transition: background 15ms linear;
|
||||
}
|
||||
|
||||
.tabs > *.active {
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.tabs > *:focus,
|
||||
.tabs > *:hover {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
`;
|
||||
@@ -164,6 +164,7 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
power_config?: PowerConfig;
|
||||
stat_soc?: string;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
|
||||
@@ -96,7 +96,10 @@ export const getEntities = (
|
||||
|
||||
const domainName = domainToName(hass.localize, computeDomain(entityId));
|
||||
|
||||
const isRTL = computeRTL(hass);
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
|
||||
+26
-9
@@ -456,11 +456,13 @@ const getIconFromTranslations = (
|
||||
};
|
||||
|
||||
export const entityIcon = async (
|
||||
hass: HomeAssistant,
|
||||
entities: HomeAssistant["entities"],
|
||||
hassConfig: HomeAssistant["config"],
|
||||
hassConnection: Connection,
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => {
|
||||
const entry = hass.entities?.[stateObj.entity_id] as
|
||||
const entry = entities?.[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
if (entry?.icon) {
|
||||
@@ -468,7 +470,14 @@ export const entityIcon = async (
|
||||
}
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
return getEntityIcon(hass, domain, stateObj, state, entry);
|
||||
return getEntityIcon(
|
||||
hassConfig,
|
||||
hassConnection,
|
||||
domain,
|
||||
stateObj,
|
||||
state,
|
||||
entry
|
||||
);
|
||||
};
|
||||
|
||||
export const entryIcon = async (
|
||||
@@ -480,11 +489,19 @@ export const entryIcon = async (
|
||||
}
|
||||
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
return getEntityIcon(hass, domain, stateObj, undefined, entry);
|
||||
return getEntityIcon(
|
||||
hass.config,
|
||||
hass.connection,
|
||||
domain,
|
||||
stateObj,
|
||||
undefined,
|
||||
entry
|
||||
);
|
||||
};
|
||||
|
||||
const getEntityIcon = async (
|
||||
hass: HomeAssistant,
|
||||
hassConfig: HomeAssistant["config"],
|
||||
hassConnection: Connection,
|
||||
domain: string,
|
||||
stateObj?: HassEntity,
|
||||
stateValue?: string,
|
||||
@@ -498,8 +515,8 @@ const getEntityIcon = async (
|
||||
let icon: string | undefined;
|
||||
if (translation_key && platform) {
|
||||
const platformIcons = await getPlatformIcons(
|
||||
hass.config,
|
||||
hass.connection,
|
||||
hassConfig,
|
||||
hassConnection,
|
||||
platform
|
||||
);
|
||||
if (platformIcons) {
|
||||
@@ -515,8 +532,8 @@ const getEntityIcon = async (
|
||||
|
||||
if (!icon) {
|
||||
const entityComponentIcons = await getComponentIcons(
|
||||
hass.connection,
|
||||
hass.config,
|
||||
hassConnection,
|
||||
hassConfig,
|
||||
domain
|
||||
);
|
||||
if (entityComponentIcons) {
|
||||
|
||||
@@ -195,6 +195,49 @@ export const localizeTriggerSource = (
|
||||
return source;
|
||||
};
|
||||
|
||||
// Mapping from a phrase key to the bare-phrase translation key (without the
|
||||
// "triggered by" prefix), used by localizeTriggerDescription below.
|
||||
const triggerDescriptionKeys: Record<
|
||||
TriggerPhraseKeys,
|
||||
| "numeric_state_of"
|
||||
| "state_of"
|
||||
| "event"
|
||||
| "time"
|
||||
| "time_pattern"
|
||||
| "homeassistant_stopping"
|
||||
| "homeassistant_starting"
|
||||
> = {
|
||||
triggered_by_numeric_state_of: "numeric_state_of",
|
||||
triggered_by_state_of: "state_of",
|
||||
triggered_by_event: "event",
|
||||
triggered_by_time_pattern: "time_pattern",
|
||||
triggered_by_time: "time",
|
||||
triggered_by_homeassistant_stopping: "homeassistant_stopping",
|
||||
triggered_by_homeassistant_starting: "homeassistant_starting",
|
||||
};
|
||||
|
||||
// Like localizeTriggerSource, but returns just the bare localized trigger
|
||||
// description (without the "triggered by" prefix). Used where the surrounding
|
||||
// template already supplies its own "triggered by" wording.
|
||||
export const localizeTriggerDescription = (
|
||||
localize: LocalizeFunc,
|
||||
source: string
|
||||
) => {
|
||||
for (const triggerPhraseKey of Object.keys(
|
||||
triggerPhrases
|
||||
) as TriggerPhraseKeys[]) {
|
||||
const phrase = triggerPhrases[triggerPhraseKey];
|
||||
if (source.startsWith(phrase)) {
|
||||
const bareKey = triggerDescriptionKeys[triggerPhraseKey];
|
||||
return source.replace(
|
||||
phrase,
|
||||
`${localize(`ui.components.logbook.${bareKey}`)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return source;
|
||||
};
|
||||
|
||||
export const localizeStateMessage = (
|
||||
hass: HomeAssistant,
|
||||
localize: LocalizeFunc,
|
||||
|
||||
@@ -154,7 +154,7 @@ export const getRecorderInfo = (conn: Connection) =>
|
||||
});
|
||||
|
||||
export const getStatisticIds = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
statistic_type?: "mean" | "sum"
|
||||
) =>
|
||||
hass.callWS<StatisticsMetaData[]>({
|
||||
@@ -227,7 +227,7 @@ export const fetchStatistic = (
|
||||
rolling_window: period.rolling_window,
|
||||
});
|
||||
|
||||
export const validateStatistics = (hass: HomeAssistant) =>
|
||||
export const validateStatistics = (hass: Pick<HomeAssistant, "callWS">) =>
|
||||
hass.callWS<StatisticsValidationResults>({
|
||||
type: "recorder/validate_statistics",
|
||||
});
|
||||
@@ -245,7 +245,10 @@ export const updateStatisticsMetadata = (
|
||||
unit_class,
|
||||
});
|
||||
|
||||
export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) =>
|
||||
export const clearStatistics = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
statistic_ids: string[]
|
||||
) =>
|
||||
hass.callWS<undefined>({
|
||||
type: "recorder/clear_statistics",
|
||||
statistic_ids,
|
||||
@@ -369,5 +372,5 @@ export const getDisplayUnit = (
|
||||
export const isExternalStatistic = (statisticsId: string): boolean =>
|
||||
statisticsId.includes(":");
|
||||
|
||||
export const updateStatisticsIssues = (hass: HomeAssistant) =>
|
||||
export const updateStatisticsIssues = (hass: Pick<HomeAssistant, "callWS">) =>
|
||||
hass.callWS<undefined>({ type: "recorder/update_statistics_issues" });
|
||||
|
||||
@@ -58,7 +58,6 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.title",
|
||||
|
||||
@@ -3,10 +3,13 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-icon-button";
|
||||
import type { DataEntryFlowStep } from "../../data/data_entry_flow";
|
||||
import {
|
||||
@@ -40,14 +43,33 @@ interface FlowUpdateEvent {
|
||||
stepPromise?: Promise<DataEntryFlowStep>;
|
||||
}
|
||||
|
||||
interface FlowStepFooterStateChangedEvent {
|
||||
loading?: boolean;
|
||||
hasPendingUpdates?: boolean;
|
||||
}
|
||||
|
||||
interface FormStepElement extends HTMLElement {
|
||||
submit(): Promise<void>;
|
||||
}
|
||||
|
||||
interface AbortStepElement extends HTMLElement {
|
||||
close(): void;
|
||||
}
|
||||
|
||||
interface CreateEntryStepElement extends HTMLElement {
|
||||
finish(): Promise<void>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"flow-update": FlowUpdateEvent;
|
||||
"flow-step-footer-state-changed": FlowStepFooterStateChangedEvent;
|
||||
}
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
"flow-update": HASSDomEvent<FlowUpdateEvent>;
|
||||
"flow-step-footer-state-changed": HASSDomEvent<FlowStepFooterStateChangedEvent>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +95,16 @@ class DataEntryFlowDialog extends LitElement {
|
||||
|
||||
@state() private _handler?: string;
|
||||
|
||||
@state() private _formStepLoading = false;
|
||||
|
||||
@state() private _createEntryHasPendingUpdates = false;
|
||||
|
||||
private _formStepRef = createRef<FormStepElement>();
|
||||
|
||||
private _abortStepRef = createRef<AbortStepElement>();
|
||||
|
||||
private _createEntryStepRef = createRef<CreateEntryStepElement>();
|
||||
|
||||
private _unsubDataEntryFlowProgress?: UnsubscribeFunc;
|
||||
|
||||
public async showDialog(params: DataEntryFlowDialogParams): Promise<void> {
|
||||
@@ -301,7 +333,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
prevent-scrim-close
|
||||
@after-show=${this._focusFormStep}
|
||||
@@ -366,11 +397,14 @@ class DataEntryFlowDialog extends LitElement {
|
||||
${this._step.type === "form"
|
||||
? html`
|
||||
<step-flow-form
|
||||
${ref(this._formStepRef)}
|
||||
autofocus
|
||||
narrow
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
@flow-step-footer-state-changed=${this
|
||||
._handleFooterStateChanged}
|
||||
></step-flow-form>
|
||||
`
|
||||
: this._step.type === "external"
|
||||
@@ -384,6 +418,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
: this._step.type === "abort"
|
||||
? html`
|
||||
<step-flow-abort
|
||||
${ref(this._abortStepRef)}
|
||||
.params=${this._params}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
@@ -411,11 +446,14 @@ class DataEntryFlowDialog extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<step-flow-create-entry
|
||||
${ref(this._createEntryStepRef)}
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.step=${this._step}
|
||||
.hass=${this.hass}
|
||||
.navigateToResult=${this._params
|
||||
.navigateToResult ?? false}
|
||||
@flow-step-footer-state-changed=${this
|
||||
._handleFooterStateChanged}
|
||||
.devices=${this._devices(
|
||||
this._params.flowConfig.showDevices,
|
||||
Object.values(this.hass.devices),
|
||||
@@ -426,10 +464,95 @@ class DataEntryFlowDialog extends LitElement {
|
||||
`}
|
||||
`}
|
||||
</div>
|
||||
${this._renderFooter()}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderFooter() {
|
||||
if (!this._step || this._loading) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
switch (this._step.type) {
|
||||
case "form":
|
||||
return html`
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.loading=${this._formStepLoading}
|
||||
@click=${this._submitFormStep}
|
||||
>
|
||||
${this._params!.flowConfig.renderShowFormStepSubmitButton(
|
||||
this.hass,
|
||||
this._step
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
`;
|
||||
case "abort":
|
||||
return this._step.reason === "missing_credentials"
|
||||
? nothing
|
||||
: html`
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._closeAbortStep}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.close"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
`;
|
||||
case "external":
|
||||
return html`
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
href=${this._step.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.external_step.open_site"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
`;
|
||||
case "create_entry": {
|
||||
const devices = this._devices(
|
||||
this._params!.flowConfig.showDevices,
|
||||
Object.values(this.hass.devices),
|
||||
this._step.result?.entry_id,
|
||||
this._params!.carryOverDevices
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._finishCreateEntryStep}
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_flow.${
|
||||
!devices.length ||
|
||||
this._createEntryHasPendingUpdates ||
|
||||
devices.some((device) => device.area_id)
|
||||
? "finish"
|
||||
: "finish_skip"
|
||||
}`
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
`;
|
||||
}
|
||||
default:
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.addEventListener("flow-update", (ev) => {
|
||||
@@ -479,6 +602,8 @@ class DataEntryFlowDialog extends LitElement {
|
||||
}
|
||||
|
||||
this._step = undefined;
|
||||
this._formStepLoading = false;
|
||||
this._createEntryHasPendingUpdates = false;
|
||||
await this.updateComplete;
|
||||
this._step = _step;
|
||||
if (
|
||||
@@ -562,20 +687,36 @@ class DataEntryFlowDialog extends LitElement {
|
||||
}
|
||||
|
||||
await this.updateComplete;
|
||||
(
|
||||
this.renderRoot.querySelector(
|
||||
"step-flow-form[autofocus]"
|
||||
) as HTMLElement | null
|
||||
)?.focus();
|
||||
this._formStepRef.value?.focus();
|
||||
};
|
||||
|
||||
private _handleFooterStateChanged = (
|
||||
ev: HASSDomEvent<FlowStepFooterStateChangedEvent>
|
||||
) => {
|
||||
if (ev.detail.loading !== undefined) {
|
||||
this._formStepLoading = ev.detail.loading;
|
||||
}
|
||||
if (ev.detail.hasPendingUpdates !== undefined) {
|
||||
this._createEntryHasPendingUpdates = ev.detail.hasPendingUpdates;
|
||||
}
|
||||
};
|
||||
|
||||
private _submitFormStep = () => {
|
||||
this._formStepRef.value?.submit();
|
||||
};
|
||||
|
||||
private _closeAbortStep = () => {
|
||||
this._abortStepRef.value?.close();
|
||||
};
|
||||
|
||||
private _finishCreateEntryStep = () => {
|
||||
this._createEntryStepRef.value?.finish();
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
.dialog-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -18,7 +18,7 @@ import "../../../components/ha-slider";
|
||||
import "../../../components/ha-time-input";
|
||||
import "../../../components/input/ha-input";
|
||||
import { isTiltOnly } from "../../../data/cover";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { ImageEntity } from "../../../data/image";
|
||||
import { computeImageUrl } from "../../../data/image";
|
||||
import "../../../panels/lovelace/components/hui-timestamp-display";
|
||||
@@ -266,7 +266,7 @@ class EntityPreviewRow extends LitElement {
|
||||
<div class="numberflex">
|
||||
<ha-slider
|
||||
labeled
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
.max=${Number(stateObj.attributes.max)}
|
||||
@@ -280,7 +280,7 @@ class EntityPreviewRow extends LitElement {
|
||||
: html`<div class="numberflex numberstate">
|
||||
<ha-input
|
||||
auto-validate
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
pattern="[0-9]+([\\.][0-9]+)?"
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
@@ -303,7 +303,7 @@ class EntityPreviewRow extends LitElement {
|
||||
<ha-select
|
||||
.label=${computeStateName(stateObj)}
|
||||
.value=${stateObj.state}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.disabled=${stateObj.state === UNAVAILABLE}
|
||||
.options=${stateObj.attributes.options?.map((option) => ({
|
||||
value: option,
|
||||
label: this.hass!.formatEntityState(stateObj, option),
|
||||
|
||||
@@ -8,7 +8,6 @@ import type { HomeAssistant } from "../../types";
|
||||
import { showConfigFlowDialog } from "./show-dialog-config-flow";
|
||||
import type { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import "../../components/ha-button";
|
||||
|
||||
@customElement("step-flow-abort")
|
||||
class StepFlowAbort extends LitElement {
|
||||
@@ -37,13 +36,6 @@ class StepFlowAbort extends LitElement {
|
||||
<div class="content">
|
||||
${this.params.flowConfig.renderAbortDescription(this.hass, this.step)}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<ha-button appearance="plain" @click=${this._flowDone}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.close"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -68,6 +60,10 @@ class StepFlowAbort extends LitElement {
|
||||
fireEvent(this, "flow-update", { step: undefined });
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this._flowDone();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return configFlowContentStyles;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-area-picker";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HaInput } from "../../components/input/ha-input";
|
||||
import { assistSatelliteSupportsSetupFlow } from "../../data/assist_satellite";
|
||||
@@ -31,6 +30,10 @@ import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voi
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
|
||||
interface DeviceTarget {
|
||||
device: string;
|
||||
}
|
||||
|
||||
@customElement("step-flow-create-entry")
|
||||
class StepFlowCreateEntry extends LitElement {
|
||||
@property({ attribute: false }) public flowConfig!: FlowConfig;
|
||||
@@ -192,20 +195,18 @@ class StepFlowCreateEntry extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<ha-button @click=${this._flowDone}
|
||||
>${localize(
|
||||
`ui.panel.config.integrations.config_flow.${
|
||||
!this.devices.length || Object.keys(this._deviceUpdate).length
|
||||
? "finish"
|
||||
: "finish_skip"
|
||||
}`
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_deviceUpdate")) {
|
||||
fireEvent(this, "flow-step-footer-state-changed", {
|
||||
hasPendingUpdates: Object.keys(this._deviceUpdate).length > 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadDomains() {
|
||||
const entries = await getConfigEntries(this.hass);
|
||||
this._domains = Object.fromEntries(
|
||||
@@ -224,18 +225,20 @@ class StepFlowCreateEntry extends LitElement {
|
||||
return updateDeviceRegistryEntry(this.hass, deviceId, {
|
||||
name_by_user: update.name,
|
||||
area_id: update.area,
|
||||
}).catch((err: any) => {
|
||||
}).catch((err: unknown) => {
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Unknown error";
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error_saving_device",
|
||||
{ error: err.message }
|
||||
{ error: message }
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
await Promise.allSettled(deviceUpdates);
|
||||
const entityUpdates: Promise<any>[] = [];
|
||||
const entityUpdates: Promise<unknown>[] = [];
|
||||
const entityIds: string[] = [];
|
||||
renamedDevices.forEach((deviceId) => {
|
||||
const entities = this._deviceEntities(
|
||||
@@ -281,8 +284,15 @@ class StepFlowCreateEntry extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public finish(): Promise<void> {
|
||||
return this._flowDone();
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: ValueChangedEvent<string>) {
|
||||
const picker = ev.currentTarget as any;
|
||||
const picker = ev.currentTarget as DeviceTarget | null;
|
||||
if (!picker) {
|
||||
return;
|
||||
}
|
||||
const device = picker.device;
|
||||
const area = ev.detail.value;
|
||||
|
||||
@@ -294,8 +304,11 @@ class StepFlowCreateEntry extends LitElement {
|
||||
}
|
||||
|
||||
private _deviceNameChanged(ev: InputEvent): void {
|
||||
const picker = ev.currentTarget as HaInput;
|
||||
const device = (picker as any).device;
|
||||
const picker = ev.currentTarget as (HaInput & DeviceTarget) | null;
|
||||
if (!picker) {
|
||||
return;
|
||||
}
|
||||
const device = picker.device;
|
||||
const name = picker.value;
|
||||
|
||||
if (!(device in this._deviceUpdate)) {
|
||||
@@ -311,22 +324,13 @@ class StepFlowCreateEntry extends LitElement {
|
||||
css`
|
||||
.devices {
|
||||
display: flex;
|
||||
margin: -4px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
gap: var(--ha-space-2);
|
||||
flex-direction: column;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.devices {
|
||||
/* header - margin content - footer */
|
||||
max-height: calc(100vh - 52px - 20px - 52px);
|
||||
}
|
||||
}
|
||||
.device {
|
||||
border: 1px solid var(--divider-color);
|
||||
padding: 6px;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
margin: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
.device-info {
|
||||
@@ -352,11 +356,6 @@ class StepFlowCreateEntry extends LitElement {
|
||||
ha-input {
|
||||
margin: var(--ha-space-2) 0;
|
||||
}
|
||||
.buttons > *:last-child {
|
||||
margin-left: auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { CSSResultGroup, TemplateResult, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { DataEntryFlowStepExternal } from "../../data/data_entry_flow";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
import "../../components/ha-button";
|
||||
|
||||
@customElement("step-flow-external")
|
||||
class StepFlowExternal extends LitElement {
|
||||
@@ -16,18 +15,9 @@ class StepFlowExternal extends LitElement {
|
||||
@property({ attribute: false }) public step!: DataEntryFlowStepExternal;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const localize = this.hass.localize;
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
${this.flowConfig.renderExternalStepDescription(this.hass, this.step)}
|
||||
<div class="open-button">
|
||||
<ha-button href=${this.step.url} target="_blank" rel="noreferrer">
|
||||
${localize(
|
||||
"ui.panel.config.integrations.config_flow.external_step.open_site"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -38,18 +28,7 @@ class StepFlowExternal extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
configFlowContentStyles,
|
||||
css`
|
||||
.open-button {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
}
|
||||
.open-button a {
|
||||
text-decoration: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return [configFlowContentStyles];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isNavigationClick } from "../../common/dom/is-navigation-click";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-alert";
|
||||
import { computeInitialHaFormData } from "../../components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../components/ha-form/ha-form";
|
||||
@@ -19,7 +19,7 @@ import { autocompleteLoginFields } from "../../data/auth";
|
||||
import type { DataEntryFlowStepForm } from "../../data/data_entry_flow";
|
||||
import { previewModule } from "../../data/preview";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import type { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
|
||||
@@ -47,6 +47,8 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
private _errors?: Record<string, string>;
|
||||
|
||||
private _formRef = createRef<HTMLElementTagNameMap["ha-form"]>();
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
@@ -88,24 +90,27 @@ class StepFlowForm extends LitElement {
|
||||
${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)}
|
||||
${this._errorMsg
|
||||
? html`<ha-alert alert-type="error">${this._errorMsg}</ha-alert>`
|
||||
: ""}
|
||||
<ha-form
|
||||
?autofocus=${this.autoFocus}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.data=${stepData}
|
||||
.disabled=${this._loading}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
.schema=${autocompleteLoginFields(
|
||||
this.handleReadOnlyFields(step.data_schema)
|
||||
)}
|
||||
.error=${this._errors}
|
||||
.computeLabel=${this._labelCallback}
|
||||
.computeHelper=${this._helperCallback}
|
||||
.computeError=${this._errorCallback}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
.context=${{ handler: step.handler }}
|
||||
></ha-form>
|
||||
: nothing}
|
||||
${step.data_schema.length
|
||||
? html`<ha-form
|
||||
${ref(this._formRef)}
|
||||
?autofocus=${this.autoFocus}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.data=${stepData}
|
||||
.disabled=${this._loading}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
.schema=${autocompleteLoginFields(
|
||||
this.handleReadOnlyFields(step.data_schema)
|
||||
)}
|
||||
.error=${this._errors}
|
||||
.computeLabel=${this._labelCallback}
|
||||
.computeHelper=${this._helperCallback}
|
||||
.computeError=${this._errorCallback}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
.context=${{ handler: step.handler }}
|
||||
></ha-form>`
|
||||
: nothing}
|
||||
</div>
|
||||
${step.preview
|
||||
? html`<div class="preview" @set-flow-errors=${this._setError}>
|
||||
@@ -125,14 +130,6 @@ class StepFlowForm extends LitElement {
|
||||
})}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="buttons">
|
||||
<ha-button @click=${this._submitStep} .loading=${this._loading}>
|
||||
${this.flowConfig.renderShowFormStepSubmitButton(
|
||||
this.hass,
|
||||
this.step
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -145,8 +142,17 @@ class StepFlowForm extends LitElement {
|
||||
this.addEventListener("keydown", this._handleKeyDown);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_loading")) {
|
||||
fireEvent(this, "flow-step-footer-state-changed", {
|
||||
loading: this._loading,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override focus(_options?: FocusOptions): void {
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
this._formRef.value?.focus();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
@@ -229,13 +235,15 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
const flowId = this.step.flow_id;
|
||||
|
||||
const toSendData = {};
|
||||
const toSendData: Record<string, unknown> = {};
|
||||
Object.keys(stepData).forEach((key) => {
|
||||
const value = stepData[key];
|
||||
const isEmpty = [undefined, ""].includes(value);
|
||||
const field = this.step.data_schema?.find((f) => f.name === key);
|
||||
const selector = (field as HaFormSelector)?.selector ?? {};
|
||||
const read_only = (Object.values(selector)[0] as any)?.read_only;
|
||||
const read_only = (
|
||||
Object.values(selector)[0] as { read_only?: boolean } | null | undefined
|
||||
)?.read_only;
|
||||
if (!isEmpty && !read_only) {
|
||||
toSendData[key] = value;
|
||||
}
|
||||
@@ -277,7 +285,13 @@ class StepFlowForm extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _stepDataChanged(ev: CustomEvent): void {
|
||||
public submit(): Promise<void> {
|
||||
return this._submitStep();
|
||||
}
|
||||
|
||||
private _stepDataChanged(
|
||||
ev: ValueChangedEvent<Record<string, unknown>>
|
||||
): void {
|
||||
this._stepData = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -321,13 +335,9 @@ class StepFlowForm extends LitElement {
|
||||
|
||||
ha-alert,
|
||||
ha-form {
|
||||
margin-top: 24px;
|
||||
margin-top: var(--ha-space-6);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../components/ha-spinner";
|
||||
import type { DataEntryFlowStep } from "../../data/data_entry_flow";
|
||||
@@ -28,7 +28,7 @@ class StepFlowLoading extends LitElement {
|
||||
return html`
|
||||
<div class="content">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
${description ? html`<div>${description}</div>` : ""}
|
||||
${description ? html`<div>${description}</div>` : nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ class StepFlowLoading extends LitElement {
|
||||
text-align: center;
|
||||
}
|
||||
ha-spinner {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class StepFlowMenu extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
${description ? html`<div class="content">${description}</div>` : ""}
|
||||
${description ? html`<div class="content">${description}</div>` : nothing}
|
||||
<div class="options">
|
||||
${options.map(
|
||||
(option) => html`
|
||||
@@ -119,17 +119,17 @@ class StepFlowMenu extends LitElement {
|
||||
configFlowContentStyles,
|
||||
css`
|
||||
.options {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 16px;
|
||||
margin-top: var(--ha-space-5);
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.content {
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: var(--ha-space-4);
|
||||
}
|
||||
.content + .options {
|
||||
margin-top: 8px;
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: 24px;
|
||||
--mdc-list-side-padding: var(--ha-space-6);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -53,7 +53,7 @@ class StepFlowProgress extends LitElement {
|
||||
text-align: center;
|
||||
}
|
||||
ha-spinner {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -2,12 +2,9 @@ import { css } from "lit";
|
||||
|
||||
export const configFlowContentStyles = css`
|
||||
h2 {
|
||||
margin: 24px 38px 0 0;
|
||||
margin: var(--ha-space-6) var(--ha-space-10) 0 0;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 38px;
|
||||
padding: 0 24px;
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: 24px;
|
||||
margin-inline-end: var(--ha-space-10);
|
||||
-moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing);
|
||||
-webkit-font-smoothing: var(--ha-font-smoothing);
|
||||
font-family: var(
|
||||
@@ -28,19 +25,8 @@ export const configFlowContentStyles = css`
|
||||
|
||||
.content,
|
||||
.preview {
|
||||
margin-top: 20px;
|
||||
padding: 0 24px;
|
||||
margin-top: var(--ha-space-5);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
margin: 8px 0 0;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
ha-markdown {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ export class ListItemsDialog
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title ?? " "}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -112,7 +112,6 @@ export class DialogEnterCode
|
||||
if (isText) {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._dialogParams.title ??
|
||||
this.hass.localize("ui.dialogs.enter_code.title")}
|
||||
@@ -150,7 +149,6 @@ export class DialogEnterCode
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._dialogParams.title ?? "Enter code"}
|
||||
width="small"
|
||||
|
||||
@@ -140,7 +140,6 @@ export class DialogForm
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
prevent-scrim-close
|
||||
|
||||
@@ -96,7 +96,6 @@ export class HaImagecropperDialog
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.dialogs.image_cropper.crop_image"
|
||||
|
||||
@@ -148,7 +148,6 @@ class DialogLightColorFavorite extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.headerTitle=${this._dialogParams?.title}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -65,7 +65,6 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._open}
|
||||
.hass=${this.hass}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.siren.advanced_controls"
|
||||
)}
|
||||
|
||||
@@ -46,8 +46,7 @@ class MoreInfoAlarmControlPanel extends LitElement {
|
||||
? html`
|
||||
<div class="status">
|
||||
<div class="icon">
|
||||
<ha-state-icon .hass=${this.hass} .stateObj=${this.stateObj}>
|
||||
</ha-state-icon>
|
||||
<ha-state-icon .stateObj=${this.stateObj}> </ha-state-icon>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-date-input";
|
||||
import "../../../components/ha-time-input";
|
||||
import { isUnavailableState, UNKNOWN } from "../../../data/entity/entity";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
|
||||
import {
|
||||
setInputDateTimeValue,
|
||||
stateToIsoDateString,
|
||||
@@ -27,7 +27,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
<ha-date-input
|
||||
.locale=${this.hass.locale}
|
||||
.value=${stateToIsoDateString(this.stateObj)}
|
||||
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._dateChanged}
|
||||
>
|
||||
</ha-date-input>
|
||||
@@ -42,7 +42,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
? this.stateObj.state.split(" ")[1]
|
||||
: this.stateObj.state}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${isUnavailableState(this.stateObj.state)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@value-changed=${this._timeChanged}
|
||||
@click=${this._stopEventPropagation}
|
||||
></ha-time-input>
|
||||
|
||||
@@ -97,10 +97,7 @@ class MoreInfoLock extends LitElement {
|
||||
<div class="status">
|
||||
<span></span>
|
||||
<div class="icon">
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-state-icon>
|
||||
<ha-state-icon .stateObj=${this.stateObj}></ha-state-icon>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -190,7 +190,6 @@ class MoreInfoWeather extends LitElement {
|
||||
<ha-state-icon
|
||||
class="weather-icon"
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>
|
||||
`}
|
||||
</div>
|
||||
|
||||
@@ -594,11 +594,13 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
? !favoritesHandler.hasCustomFavorites(favoritesContext.entry)
|
||||
: false;
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.width=${this._fill ? "full" : this.large ? "large" : "medium"}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -242,7 +242,6 @@ export class QuickBar extends LitElement {
|
||||
<ha-adaptive-dialog
|
||||
without-header
|
||||
flexcontent
|
||||
.hass=${this.hass}
|
||||
aria-label=${this.hass.localize("ui.dialogs.quick-bar.title")}
|
||||
.open=${this._open}
|
||||
hideActions
|
||||
@@ -253,7 +252,6 @@ export class QuickBar extends LitElement {
|
||||
${!this._loading && this._opened
|
||||
? html`<ha-picker-combo-box
|
||||
id="combo-box"
|
||||
.hass=${this.hass}
|
||||
@index-selected=${this._handleItemSelected}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.dialogs.quick-bar.nothing_found"
|
||||
|
||||
@@ -76,7 +76,6 @@ class DialogRestartWait extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.headerTitle=${this._title}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -109,7 +109,6 @@ class DialogRestart extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._dialogOpen}
|
||||
header-title=${dialogTitle}
|
||||
allow-mode-change
|
||||
|
||||
@@ -165,7 +165,6 @@ class DialogEditSidebar extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${dialogTitle}
|
||||
header-subtitle=${!this._migrateToUserData
|
||||
|
||||
@@ -70,7 +70,6 @@ export class TTSTryDialog extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize("ui.dialogs.tts-try.header")}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -29,7 +29,6 @@ class DialogBox extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize("ui.dialogs.update_backup.title")}
|
||||
width="small"
|
||||
|
||||
@@ -143,7 +143,6 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title="Voice Satellite setup"
|
||||
prevent-scrim-close
|
||||
|
||||
@@ -98,12 +98,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
@closed=${this._dialogClosed}
|
||||
flexcontent
|
||||
>
|
||||
<ha-dialog .open=${this._open} @closed=${this._dialogClosed} flexcontent>
|
||||
<ha-dialog-header slot="header">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
|
||||
@@ -552,7 +552,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
</hass-tabs-subpage>
|
||||
${this.showFilters && !showPane
|
||||
? html`<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${true}
|
||||
width="full"
|
||||
header-title=${localize("ui.components.subpage-data-table.filters")}
|
||||
|
||||
@@ -41,7 +41,6 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._params.title}
|
||||
width="small"
|
||||
|
||||
@@ -66,7 +66,6 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
const stateObj = this.hass.states[this._calendarId!];
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this._data!.summary}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -149,7 +149,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${this.hass.localize(
|
||||
`ui.components.calendar.event.${isCreate ? "add" : "edit"}`
|
||||
|
||||
@@ -24,8 +24,8 @@ import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||
import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
CalendarEventSubscription,
|
||||
CalendarEventApiData,
|
||||
CalendarEventSubscription,
|
||||
} from "../../data/calendar";
|
||||
import {
|
||||
getCalendars,
|
||||
@@ -144,7 +144,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
<ha-state-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${selCal}
|
||||
style="--icon-primary-color: ${selCal.backgroundColor}"
|
||||
></ha-state-icon>
|
||||
|
||||
@@ -98,7 +98,6 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
: "";
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
@closed=${this._abortDialog}
|
||||
.preventScrimClose=${!!this._domain ||
|
||||
|
||||
@@ -70,7 +70,6 @@ class AppsRegistriesDialog extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
@closed=${this._dialogClosed}
|
||||
header-title=${this.hass.localize(
|
||||
|
||||
@@ -87,7 +87,6 @@ class DialogAreasFloorsOrder extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${dialogTitle}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -108,7 +108,6 @@ class DialogFloorDetail extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${entry
|
||||
? this.hass.localize("ui.panel.config.floors.editor.update_floor")
|
||||
|
||||
@@ -76,6 +76,7 @@ import type {
|
||||
} from "../../../../data/script";
|
||||
import { getActionType, isAction } from "../../../../data/script";
|
||||
import { describeAction } from "../../../../data/script_i18n";
|
||||
import type { TargetSelector } from "../../../../data/selector";
|
||||
import { callExecuteScript } from "../../../../data/service";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -266,7 +267,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderRow(row = true) {
|
||||
private _renderRow() {
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
const action = type === "service" && (this.action as ServiceAction).action;
|
||||
@@ -288,6 +289,12 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
? { device_id: (this.action as DeviceAction).device_id }
|
||||
: undefined;
|
||||
|
||||
const serviceTargetSpec =
|
||||
type === "service" && action
|
||||
? this.hass.services?.[computeDomain(action)]?.[computeObjectId(action)]
|
||||
?.target
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "service" && "action" in this.action && this.action.action
|
||||
? html`
|
||||
@@ -317,7 +324,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
)
|
||||
)}
|
||||
${target !== undefined || (actionHasTarget && !this._isNew)
|
||||
? this._renderTargets(target, actionHasTarget && !this._isNew)
|
||||
? this._renderTargets(
|
||||
target,
|
||||
actionHasTarget && !this._isNew,
|
||||
serviceTargetSpec
|
||||
)
|
||||
: nothing}
|
||||
${type !== "condition" &&
|
||||
(this.action as NonConditionAction).continue_on_error === true
|
||||
@@ -331,7 +342,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="svg-icon">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.continue_on_error"
|
||||
"ui.panel.config.automation.editor.actions.continue_on_error_description"
|
||||
)}
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
@@ -339,10 +350,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
<ha-automation-row-event-chip
|
||||
.show=${this._running}
|
||||
.variant=${this._runResult?.variant}
|
||||
.slot=${row ? "event" : ""}
|
||||
slot="event"
|
||||
aria-live="polite"
|
||||
.interactive=${!!this._runResult?.details}
|
||||
class=${row ? "" : "event-chip"}
|
||||
class="event-chip"
|
||||
@click=${this._showRunResultDetails}
|
||||
@keydown=${this._showRunResultDetails}
|
||||
>
|
||||
@@ -640,7 +651,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
left-chevron
|
||||
@expanded-changed=${this._expansionPanelChanged}
|
||||
>
|
||||
${this._renderRow(false)}
|
||||
${this._renderRow()}
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
</ha-card>
|
||||
@@ -681,11 +692,16 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(target?: HassServiceTarget, targetRequired = false) =>
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"]
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
@@ -1148,7 +1164,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
overflowStyles,
|
||||
css`
|
||||
ha-svg-icon.arrow-right {
|
||||
--icon-primary-color: var(--ha-color-fill-neutral-normal-resting);
|
||||
--icon-primary-color: var(--ha-color-fill-neutral-loud-resting);
|
||||
}
|
||||
ha-svg-icon#svg-icon {
|
||||
--icon-primary-color: var(--ha-color-fill-neutral-loud-active);
|
||||
|
||||
@@ -598,7 +598,6 @@ class DialogAddAutomationElement
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
width="large"
|
||||
.open=${this._open}
|
||||
@closed=${this._handleClosed}
|
||||
@@ -971,7 +970,14 @@ class DialogAddAutomationElement
|
||||
|
||||
subtitle = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
|
||||
.join(
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? " ◂ "
|
||||
: " ▸ "
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -769,7 +769,6 @@ export default class HaAutomationAddFromTarget extends LitElement {
|
||||
private _renderEntityIcon =
|
||||
(stateObj: HassEntity) => (slot: string | undefined) =>
|
||||
html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
slot=${ifDefined(slot)}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>`;
|
||||
|
||||
@@ -300,7 +300,10 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
let showEntityId = false;
|
||||
|
||||
if (type === "area" || type === "floor") {
|
||||
rtl = computeRTL(this.hass);
|
||||
rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
hasFloor =
|
||||
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user