mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-03 18:34:52 +00:00
Compare commits
50 Commits
20250531.4
...
move-defau
Author | SHA1 | Date | |
---|---|---|---|
![]() |
365db51526 | ||
![]() |
8eb7fe8b0a | ||
![]() |
c8c2966d34 | ||
![]() |
a8768a5d9d | ||
![]() |
02bb7086e7 | ||
![]() |
42d8b2ae19 | ||
![]() |
e08f4a6bba | ||
![]() |
2e6c35d977 | ||
![]() |
17305a818b | ||
![]() |
08389dad04 | ||
![]() |
ab6ace46b5 | ||
![]() |
535dedbbc4 | ||
![]() |
412eb0c647 | ||
![]() |
87c8ebd493 | ||
![]() |
6e49f89126 | ||
![]() |
a099e65a9d | ||
![]() |
11e4a9f056 | ||
![]() |
b617299eee | ||
![]() |
768f27b1b9 | ||
![]() |
5ed816df6d | ||
![]() |
6692ac7517 | ||
![]() |
65499db0cb | ||
![]() |
11a1eabf61 | ||
![]() |
b30fa122ba | ||
![]() |
6730d08b85 | ||
![]() |
67003d6fd1 | ||
![]() |
414d46be65 | ||
![]() |
1485d1a1de | ||
![]() |
fd13e41524 | ||
![]() |
77f7ca0368 | ||
![]() |
7471250a07 | ||
![]() |
8b0a63d791 | ||
![]() |
57ffa814ed | ||
![]() |
16e20456e2 | ||
![]() |
9c0ce41ebb | ||
![]() |
b458a1d7c6 | ||
![]() |
0eaeeb1141 | ||
![]() |
b7e63e697f | ||
![]() |
06db0f4b98 | ||
![]() |
d33636c6fb | ||
![]() |
bbb546159c | ||
![]() |
e8fc36026a | ||
![]() |
38f8c804af | ||
![]() |
7c5bf26240 | ||
![]() |
189067d14b | ||
![]() |
e79e0f77b8 | ||
![]() |
b226e5c697 | ||
![]() |
52ad31601c | ||
![]() |
cba3e4df7f | ||
![]() |
3532cfa974 |
@@ -1 +1 @@
|
|||||||
yarn run lint-staged --relative --shell "/bin/bash"
|
yarn run lint-staged --relative
|
||||||
|
@@ -1,7 +1,30 @@
|
|||||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||||
|
|
||||||
|
let changeFunction;
|
||||||
|
|
||||||
export const mockFrontend = (hass: MockHomeAssistant) => {
|
export const mockFrontend = (hass: MockHomeAssistant) => {
|
||||||
hass.mockWS("frontend/get_user_data", () => ({
|
hass.mockWS("frontend/get_user_data", () => ({
|
||||||
value: null,
|
value: null,
|
||||||
}));
|
}));
|
||||||
|
hass.mockWS("frontend/set_user_data", ({ key, value }) => {
|
||||||
|
if (key === "sidebar") {
|
||||||
|
changeFunction?.({
|
||||||
|
value: {
|
||||||
|
panelOrder: value.panelOrder || [],
|
||||||
|
hiddenPanels: value.hiddenPanels || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => {
|
||||||
|
changeFunction = onChange;
|
||||||
|
onChange?.({
|
||||||
|
value: {
|
||||||
|
panelOrder: [],
|
||||||
|
hiddenPanels: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
return () => {};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
32
package.json
32
package.json
@@ -26,7 +26,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.27.1",
|
"@babel/runtime": "7.27.4",
|
||||||
"@braintree/sanitize-url": "7.1.1",
|
"@braintree/sanitize-url": "7.1.1",
|
||||||
"@codemirror/autocomplete": "6.18.6",
|
"@codemirror/autocomplete": "6.18.6",
|
||||||
"@codemirror/commands": "6.8.1",
|
"@codemirror/commands": "6.8.1",
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"@codemirror/legacy-modes": "6.5.1",
|
"@codemirror/legacy-modes": "6.5.1",
|
||||||
"@codemirror/search": "6.5.11",
|
"@codemirror/search": "6.5.11",
|
||||||
"@codemirror/state": "6.5.2",
|
"@codemirror/state": "6.5.2",
|
||||||
"@codemirror/view": "6.36.8",
|
"@codemirror/view": "6.37.1",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||||
"@formatjs/intl-displaynames": "6.8.11",
|
"@formatjs/intl-displaynames": "6.8.11",
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
"fuse.js": "7.1.0",
|
"fuse.js": "7.1.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"gulp-zopfli-green": "6.0.2",
|
"gulp-zopfli-green": "6.0.2",
|
||||||
"hls.js": "1.6.2",
|
"hls.js": "1.6.4",
|
||||||
"home-assistant-js-websocket": "9.5.0",
|
"home-assistant-js-websocket": "9.5.0",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.2",
|
||||||
"intl-messageformat": "10.7.16",
|
"intl-messageformat": "10.7.16",
|
||||||
@@ -149,20 +149,20 @@
|
|||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.27.1",
|
"@babel/core": "7.27.4",
|
||||||
"@babel/helper-define-polyfill-provider": "0.6.4",
|
"@babel/helper-define-polyfill-provider": "0.6.4",
|
||||||
"@babel/plugin-transform-runtime": "7.27.1",
|
"@babel/plugin-transform-runtime": "7.27.4",
|
||||||
"@babel/preset-env": "7.27.2",
|
"@babel/preset-env": "7.27.2",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.20.1",
|
"@bundle-stats/plugin-webpack-filter": "4.20.2",
|
||||||
"@lokalise/node-api": "14.7.0",
|
"@lokalise/node-api": "14.8.0",
|
||||||
"@octokit/auth-oauth-device": "8.0.1",
|
"@octokit/auth-oauth-device": "8.0.1",
|
||||||
"@octokit/plugin-retry": "8.0.1",
|
"@octokit/plugin-retry": "8.0.1",
|
||||||
"@octokit/rest": "21.1.1",
|
"@octokit/rest": "22.0.0",
|
||||||
"@rsdoctor/rspack-plugin": "1.1.2",
|
"@rsdoctor/rspack-plugin": "1.1.2",
|
||||||
"@rspack/cli": "1.3.11",
|
"@rspack/cli": "1.3.12",
|
||||||
"@rspack/core": "1.3.11",
|
"@rspack/core": "1.3.12",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.21",
|
"@types/chromecast-caf-receiver": "6.0.22",
|
||||||
"@types/chromecast-caf-sender": "1.0.11",
|
"@types/chromecast-caf-sender": "1.0.11",
|
||||||
"@types/color-name": "2.0.0",
|
"@types/color-name": "2.0.0",
|
||||||
"@types/glob": "8.1.0",
|
"@types/glob": "8.1.0",
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"browserslist-useragent-regexp": "4.1.3",
|
"browserslist-useragent-regexp": "4.1.3",
|
||||||
"del": "8.0.0",
|
"del": "8.0.0",
|
||||||
"eslint": "9.27.0",
|
"eslint": "9.28.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-prettier": "10.1.5",
|
"eslint-config-prettier": "10.1.5",
|
||||||
"eslint-import-resolver-webpack": "0.13.10",
|
"eslint-import-resolver-webpack": "0.13.10",
|
||||||
@@ -196,7 +196,7 @@
|
|||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.3.0",
|
"fs-extra": "11.3.0",
|
||||||
"glob": "11.0.2",
|
"glob": "11.0.2",
|
||||||
"gulp": "5.0.0",
|
"gulp": "5.0.1",
|
||||||
"gulp-brotli": "3.0.0",
|
"gulp-brotli": "3.0.0",
|
||||||
"gulp-json-transform": "0.5.0",
|
"gulp-json-transform": "0.5.0",
|
||||||
"gulp-rename": "2.0.0",
|
"gulp-rename": "2.0.0",
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "15.5.2",
|
"lint-staged": "16.1.0",
|
||||||
"lit-analyzer": "2.0.3",
|
"lit-analyzer": "2.0.3",
|
||||||
"lodash.merge": "4.6.2",
|
"lodash.merge": "4.6.2",
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
@@ -218,7 +218,7 @@
|
|||||||
"terser-webpack-plugin": "5.3.14",
|
"terser-webpack-plugin": "5.3.14",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"typescript-eslint": "8.32.1",
|
"typescript-eslint": "8.33.0",
|
||||||
"vite-tsconfig-paths": "5.1.4",
|
"vite-tsconfig-paths": "5.1.4",
|
||||||
"vitest": "3.1.4",
|
"vitest": "3.1.4",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
"clean-css": "5.3.3",
|
"clean-css": "5.3.3",
|
||||||
"@lit/reactive-element": "2.1.0",
|
"@lit/reactive-element": "2.1.0",
|
||||||
"@fullcalendar/daygrid": "6.1.17",
|
"@fullcalendar/daygrid": "6.1.17",
|
||||||
"globals": "16.1.0",
|
"globals": "16.2.0",
|
||||||
"tslib": "2.8.1",
|
"tslib": "2.8.1",
|
||||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
|
||||||
},
|
},
|
||||||
|
4
src/common/entity/valid_service_id.ts
Normal file
4
src/common/entity/valid_service_id.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
const validServiceId = /^(\w+)\.(\w+)$/;
|
||||||
|
|
||||||
|
export const isValidServiceId = (actionId: string) =>
|
||||||
|
validServiceId.test(actionId);
|
@@ -1,9 +1,19 @@
|
|||||||
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
// https://gist.github.com/hagemann/382adfc57adbd5af078dc93feef01fe1
|
||||||
export const slugify = (value: string, delimiter = "_") => {
|
export const slugify = (value: string, delimiter = "_") => {
|
||||||
const a =
|
const a =
|
||||||
"àáâäæãåāăąçćčđďèéêëēėęěğǵḧîïíīįìıİłḿñńǹňôöòóœøōõőṕŕřßśšşșťțûüùúūǘůűųẃẍÿýžźż·";
|
"àáâäæãåāăąабçćčđďдèéêëēėęěеёэфğǵгḧхîïíīįìıİийкłлḿмñńǹňнôöòóœøōõőоṕпŕřрßśšşșсťțтûüùúūǘůűųувẃẍÿýыžźżз·";
|
||||||
const b = `aaaaaaaaaacccddeeeeeeeegghiiiiiiiilmnnnnoooooooooprrsssssttuuuuuuuuuwxyyzzz${delimiter}`;
|
const b = `aaaaaaaaaaabcccdddeeeeeeeeeeefggghhiiiiiiiiijkllmmnnnnnoooooooooopprrrsssssstttuuuuuuuuuuvwxyyyzzzz${delimiter}`;
|
||||||
const p = new RegExp(a.split("").join("|"), "g");
|
const p = new RegExp(a.split("").join("|"), "g");
|
||||||
|
const complex_cyrillic = {
|
||||||
|
ж: "zh",
|
||||||
|
х: "kh",
|
||||||
|
ц: "ts",
|
||||||
|
ч: "ch",
|
||||||
|
ш: "sh",
|
||||||
|
щ: "shch",
|
||||||
|
ю: "iu",
|
||||||
|
я: "ia",
|
||||||
|
};
|
||||||
|
|
||||||
let slugified;
|
let slugified;
|
||||||
|
|
||||||
@@ -14,6 +24,7 @@ export const slugify = (value: string, delimiter = "_") => {
|
|||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
|
||||||
|
.replace(/[а-я]/g, (c) => complex_cyrillic[c] || "") // Replace some cyrillic characters
|
||||||
.replace(/(\d),(?=\d)/g, "$1") // Remove Commas between numbers
|
.replace(/(\d),(?=\d)/g, "$1") // Remove Commas between numbers
|
||||||
.replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
|
.replace(/[^a-z0-9]+/g, delimiter) // Replace all non-word characters
|
||||||
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
|
.replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
|
||||||
|
@@ -220,11 +220,11 @@ export class HaChartBase extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const datasets = ensureArray(this.data);
|
const datasets = ensureArray(this.data);
|
||||||
const items = (legend.data ||
|
const items: LegendComponentOption["data"] =
|
||||||
datasets
|
legend.data ||
|
||||||
|
((datasets
|
||||||
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
.filter((d) => (d.data as any[])?.length && (d.id || d.name))
|
||||||
.map((d) => d.name ?? d.id) ||
|
.map((d) => d.name ?? d.id) || []) as string[]);
|
||||||
[]) as string[];
|
|
||||||
|
|
||||||
const isMobile = window.matchMedia(
|
const isMobile = window.matchMedia(
|
||||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
@@ -239,20 +239,32 @@ export class HaChartBase extends LitElement {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
${items.map((item: string, index: number) => {
|
${items.map((item, index) => {
|
||||||
if (!this.expandLegend && index >= overflowLimit) {
|
if (!this.expandLegend && index >= overflowLimit) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
let itemStyle: Record<string, any> = {};
|
||||||
|
let name = "";
|
||||||
|
if (typeof item === "string") {
|
||||||
|
name = item;
|
||||||
const dataset = datasets.find(
|
const dataset = datasets.find(
|
||||||
(d) => d.id === item || d.name === item
|
(d) => d.id === item || d.name === item
|
||||||
);
|
);
|
||||||
const color = dataset?.color as string;
|
itemStyle = {
|
||||||
const borderColor = dataset?.itemStyle?.borderColor as string;
|
color: dataset?.color as string,
|
||||||
|
...(dataset?.itemStyle as { borderColor?: string }),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
name = item.name ?? "";
|
||||||
|
itemStyle = item.itemStyle ?? {};
|
||||||
|
}
|
||||||
|
const color = itemStyle?.color as string;
|
||||||
|
const borderColor = itemStyle?.borderColor as string;
|
||||||
return html`<li
|
return html`<li
|
||||||
.name=${item}
|
.name=${name}
|
||||||
@click=${this._legendClick}
|
@click=${this._legendClick}
|
||||||
class=${classMap({ hidden: this._hiddenDatasets.has(item) })}
|
class=${classMap({ hidden: this._hiddenDatasets.has(name) })}
|
||||||
.title=${item}
|
.title=${name}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bullet"
|
class="bullet"
|
||||||
@@ -261,7 +273,7 @@ export class HaChartBase extends LitElement {
|
|||||||
borderColor: borderColor || color,
|
borderColor: borderColor || color,
|
||||||
})}
|
})}
|
||||||
></div>
|
></div>
|
||||||
<div class="label">${item}</div>
|
<div class="label">${name}</div>
|
||||||
</li>`;
|
</li>`;
|
||||||
})}
|
})}
|
||||||
${items.length > overflowLimit
|
${items.length > overflowLimit
|
||||||
@@ -482,6 +494,13 @@ export class HaChartBase extends LitElement {
|
|||||||
smooth: false,
|
smooth: false,
|
||||||
},
|
},
|
||||||
bar: { itemStyle: { barBorderWidth: 1.5 } },
|
bar: { itemStyle: { barBorderWidth: 1.5 } },
|
||||||
|
graph: {
|
||||||
|
label: {
|
||||||
|
color: style.getPropertyValue("--primary-text-color"),
|
||||||
|
textBorderColor: style.getPropertyValue("--primary-background-color"),
|
||||||
|
textBorderWidth: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
categoryAxis: {
|
categoryAxis: {
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
|
@@ -82,6 +82,8 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
|
|
||||||
private _chartTime: Date = new Date();
|
private _chartTime: Date = new Date();
|
||||||
|
|
||||||
|
private _previousYAxisLabelValue = 0;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-chart-base
|
<ha-chart-base
|
||||||
@@ -258,32 +260,7 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
},
|
},
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
margin: 5,
|
margin: 5,
|
||||||
formatter: (value: number) => {
|
formatter: this._formatYAxisLabel,
|
||||||
const formatOptions =
|
|
||||||
value >= 1 || value <= -1
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
// show the first significant digit for tiny values
|
|
||||||
maximumFractionDigits: Math.max(
|
|
||||||
2,
|
|
||||||
-Math.floor(Math.log10(Math.abs(value % 1 || 1)))
|
|
||||||
),
|
|
||||||
};
|
|
||||||
const label = formatNumber(
|
|
||||||
value,
|
|
||||||
this.hass.locale,
|
|
||||||
formatOptions
|
|
||||||
);
|
|
||||||
const width = measureTextWidth(label, 12) + 5;
|
|
||||||
if (width > this._yWidth) {
|
|
||||||
this._yWidth = width;
|
|
||||||
fireEvent(this, "y-width-changed", {
|
|
||||||
value: this._yWidth,
|
|
||||||
chartIndex: this.chartIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return label;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} as YAXisOption,
|
} as YAXisOption,
|
||||||
legend: {
|
legend: {
|
||||||
@@ -745,6 +722,33 @@ export class StateHistoryChartLine extends LitElement {
|
|||||||
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
|
this._visualMap = visualMap.length > 0 ? visualMap : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _formatYAxisLabel = (value: number) => {
|
||||||
|
const formatOptions =
|
||||||
|
value >= 1 || value <= -1
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
// show the first significant digit for tiny values
|
||||||
|
maximumFractionDigits: Math.max(
|
||||||
|
2,
|
||||||
|
// use the difference to the previous value to determine the number of significant digits #25526
|
||||||
|
-Math.floor(
|
||||||
|
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const label = formatNumber(value, this.hass.locale, formatOptions);
|
||||||
|
const width = measureTextWidth(label, 12) + 5;
|
||||||
|
if (width > this._yWidth) {
|
||||||
|
this._yWidth = width;
|
||||||
|
fireEvent(this, "y-width-changed", {
|
||||||
|
value: this._yWidth,
|
||||||
|
chartIndex: this.chartIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._previousYAxisLabelValue = value;
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
private _clampYAxis(value?: number | ((values: any) => number)) {
|
private _clampYAxis(value?: number | ((values: any) => number)) {
|
||||||
if (this.logarithmicScale) {
|
if (this.logarithmicScale) {
|
||||||
// log(0) is -Infinity, so we need to set a minimum value
|
// log(0) is -Infinity, so we need to set a minimum value
|
||||||
|
@@ -740,6 +740,7 @@ export class HaDataTable extends LitElement {
|
|||||||
}, {});
|
}, {});
|
||||||
const groupedItems: DataTableRowData[] = [];
|
const groupedItems: DataTableRowData[] = [];
|
||||||
Object.entries(sorted).forEach(([groupName, rows]) => {
|
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||||
|
const collapsed = collapsedGroups.includes(groupName);
|
||||||
groupedItems.push({
|
groupedItems.push({
|
||||||
append: true,
|
append: true,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
@@ -751,9 +752,10 @@ export class HaDataTable extends LitElement {
|
|||||||
>
|
>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.path=${mdiChevronUp}
|
.path=${mdiChevronUp}
|
||||||
class=${collapsedGroups.includes(groupName)
|
.label=${this.hass.localize(
|
||||||
? "collapsed"
|
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
|
||||||
: ""}
|
)}
|
||||||
|
class=${collapsed ? "collapsed" : ""}
|
||||||
>
|
>
|
||||||
</ha-icon-button>
|
</ha-icon-button>
|
||||||
${groupName === UNDEFINED_GROUP_KEY
|
${groupName === UNDEFINED_GROUP_KEY
|
||||||
|
@@ -23,7 +23,10 @@ import type { HomeAssistant } from "../../types";
|
|||||||
import "../ha-combo-box-item";
|
import "../ha-combo-box-item";
|
||||||
import "../ha-generic-picker";
|
import "../ha-generic-picker";
|
||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
import type {
|
||||||
|
PickerComboBoxItem,
|
||||||
|
PickerComboBoxSearchFn,
|
||||||
|
} from "../ha-picker-combo-box";
|
||||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
@@ -51,6 +54,9 @@ export class HaEntityPicker extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||||
public allowCustomEntity;
|
public allowCustomEntity;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "show-entity-id" })
|
||||||
|
public showEntityId = false;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
@@ -166,11 +172,15 @@ export class HaEntityPicker extends LitElement {
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private get _showEntityId() {
|
||||||
|
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
||||||
|
}
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
||||||
item,
|
item,
|
||||||
{ index }
|
{ index }
|
||||||
) => {
|
) => {
|
||||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
const showEntityId = this._showEntityId;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||||
@@ -390,6 +400,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
.autofocus=${this.autofocus}
|
.autofocus=${this.autofocus}
|
||||||
.allowCustomValue=${this.allowCustomEntity}
|
.allowCustomValue=${this.allowCustomEntity}
|
||||||
.label=${this.label}
|
.label=${this.label}
|
||||||
|
.helper=${this.helper}
|
||||||
.searchLabel=${this.searchLabel}
|
.searchLabel=${this.searchLabel}
|
||||||
.notFoundLabel=${notFoundLabel}
|
.notFoundLabel=${notFoundLabel}
|
||||||
.placeholder=${placeholder}
|
.placeholder=${placeholder}
|
||||||
@@ -398,6 +409,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
@@ -405,6 +417,23 @@ export class HaEntityPicker extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _searchFn: PickerComboBoxSearchFn<EntityComboBoxItem> = (
|
||||||
|
search,
|
||||||
|
filteredItems
|
||||||
|
) => {
|
||||||
|
// If there is exact match for entity id, put it first
|
||||||
|
const index = filteredItems.findIndex(
|
||||||
|
(item) => item.stateObj?.entity_id === search
|
||||||
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
return filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [exactMatch] = filteredItems.splice(index, 1);
|
||||||
|
filteredItems.unshift(exactMatch);
|
||||||
|
return filteredItems;
|
||||||
|
};
|
||||||
|
|
||||||
public async open() {
|
public async open() {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
await this._picker?.open();
|
await this._picker?.open();
|
||||||
|
@@ -25,7 +25,10 @@ import "../ha-generic-picker";
|
|||||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||||
import "../ha-icon-button";
|
import "../ha-icon-button";
|
||||||
import "../ha-input-helper-text";
|
import "../ha-input-helper-text";
|
||||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
import type {
|
||||||
|
PickerComboBoxItem,
|
||||||
|
PickerComboBoxSearchFn,
|
||||||
|
} from "../ha-picker-combo-box";
|
||||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
@@ -470,6 +473,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
.getItems=${this._getItems}
|
.getItems=${this._getItems}
|
||||||
.getAdditionalItems=${this._getAdditionalItems}
|
.getAdditionalItems=${this._getAdditionalItems}
|
||||||
.hideClearIcon=${this.hideClearIcon}
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
|
.searchFn=${this._searchFn}
|
||||||
.valueRenderer=${this._valueRenderer}
|
.valueRenderer=${this._valueRenderer}
|
||||||
@value-changed=${this._valueChanged}
|
@value-changed=${this._valueChanged}
|
||||||
>
|
>
|
||||||
@@ -477,6 +481,24 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _searchFn: PickerComboBoxSearchFn<StatisticComboBoxItem> = (
|
||||||
|
search,
|
||||||
|
filteredItems
|
||||||
|
) => {
|
||||||
|
// If there is exact match for entity id or statistic id, put it first
|
||||||
|
const index = filteredItems.findIndex(
|
||||||
|
(item) =>
|
||||||
|
item.stateObj?.entity_id === search || item.statistic_id === search
|
||||||
|
);
|
||||||
|
if (index === -1) {
|
||||||
|
return filteredItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [exactMatch] = filteredItems.splice(index, 1);
|
||||||
|
filteredItems.unshift(exactMatch);
|
||||||
|
return filteredItems;
|
||||||
|
};
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const value = ev.detail.value;
|
const value = ev.detail.value;
|
||||||
|
@@ -34,6 +34,8 @@ const getWarning = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
|||||||
export class HaForm extends LitElement implements HaFormElement {
|
export class HaForm extends LitElement implements HaFormElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public data!: HaFormDataContainer;
|
@property({ attribute: false }) public data!: HaFormDataContainer;
|
||||||
|
|
||||||
@property({ attribute: false }) public schema!: readonly HaFormSchema[];
|
@property({ attribute: false }) public schema!: readonly HaFormSchema[];
|
||||||
@@ -135,6 +137,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
|||||||
? html`<ha-selector
|
? html`<ha-selector
|
||||||
.schema=${item}
|
.schema=${item}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
.name=${item.name}
|
.name=${item.name}
|
||||||
.selector=${item.selector}
|
.selector=${item.selector}
|
||||||
.value=${getValue(this.data, item)}
|
.value=${getValue(this.data, item)}
|
||||||
|
@@ -12,6 +12,7 @@ import "./ha-picker-combo-box";
|
|||||||
import type {
|
import type {
|
||||||
HaPickerComboBox,
|
HaPickerComboBox,
|
||||||
PickerComboBoxItem,
|
PickerComboBoxItem,
|
||||||
|
PickerComboBoxSearchFn,
|
||||||
} from "./ha-picker-combo-box";
|
} from "./ha-picker-combo-box";
|
||||||
import "./ha-picker-field";
|
import "./ha-picker-field";
|
||||||
import type { HaPickerField, PickerValueRenderer } from "./ha-picker-field";
|
import type { HaPickerField, PickerValueRenderer } from "./ha-picker-field";
|
||||||
@@ -57,6 +58,9 @@ export class HaGenericPicker extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public valueRenderer?: PickerValueRenderer;
|
public valueRenderer?: PickerValueRenderer;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||||
|
|
||||||
@property({ attribute: "not-found-label", type: String })
|
@property({ attribute: "not-found-label", type: String })
|
||||||
public notFoundLabel?: string;
|
public notFoundLabel?: string;
|
||||||
|
|
||||||
@@ -68,7 +72,9 @@ export class HaGenericPicker extends LitElement {
|
|||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
${this.label
|
||||||
|
? html`<label ?disabled=${this.disabled}>${this.label}</label>`
|
||||||
|
: nothing}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
${!this._opened
|
${!this._opened
|
||||||
? html`
|
? html`
|
||||||
@@ -102,16 +108,19 @@ export class HaGenericPicker extends LitElement {
|
|||||||
.notFoundLabel=${this.notFoundLabel}
|
.notFoundLabel=${this.notFoundLabel}
|
||||||
.getItems=${this.getItems}
|
.getItems=${this.getItems}
|
||||||
.getAdditionalItems=${this.getAdditionalItems}
|
.getAdditionalItems=${this.getAdditionalItems}
|
||||||
|
.searchFn=${this.searchFn}
|
||||||
></ha-picker-combo-box>
|
></ha-picker-combo-box>
|
||||||
`}
|
`}
|
||||||
${this._renderHelper()}
|
|
||||||
</div>
|
</div>
|
||||||
|
${this._renderHelper()}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderHelper() {
|
private _renderHelper() {
|
||||||
return this.helper
|
return this.helper
|
||||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||||
|
>${this.helper}</ha-input-helper-text
|
||||||
|
>`
|
||||||
: nothing;
|
: nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,10 +169,17 @@ export class HaGenericPicker extends LitElement {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
label[disabled] {
|
||||||
|
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6));
|
||||||
|
}
|
||||||
label {
|
label {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
}
|
}
|
||||||
|
ha-input-helper-text {
|
||||||
|
display: block;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-input-helper-text")
|
@customElement("ha-input-helper-text")
|
||||||
class InputHelperText extends LitElement {
|
class InputHelperText extends LitElement {
|
||||||
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`<slot></slot>`;
|
return html`<slot></slot>`;
|
||||||
}
|
}
|
||||||
@@ -18,6 +20,9 @@ class InputHelperText extends LitElement {
|
|||||||
padding-inline-start: 16px;
|
padding-inline-start: 16px;
|
||||||
padding-inline-end: 16px;
|
padding-inline-end: 16px;
|
||||||
}
|
}
|
||||||
|
:host([disabled]) {
|
||||||
|
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.6));
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -262,7 +262,7 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return items.sort((a, b) =>
|
return visibleItems.sort((a, b) =>
|
||||||
a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value)
|
a.disableSorting && !b.disableSorting ? -1 : compare(a.value, b.value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -77,7 +77,7 @@ export class HaMarkdown extends LitElement {
|
|||||||
pre {
|
pre {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
line-height: 1.45;
|
line-height: var(--ha-line-height-condensed);
|
||||||
font-family: var(--ha-font-family-code);
|
font-family: var(--ha-font-family-code);
|
||||||
}
|
}
|
||||||
h1,
|
h1,
|
||||||
|
@@ -85,7 +85,9 @@ class HaMultiTextField extends LitElement {
|
|||||||
</ha-button>
|
</ha-button>
|
||||||
</div>
|
</div>
|
||||||
${this.helper
|
${this.helper
|
||||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||||
|
>${this.helper}</ha-input-helper-text
|
||||||
|
>`
|
||||||
: nothing}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,7 @@ const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
|||||||
path: `/${panel.url_path}`,
|
path: `/${panel.url_path}`,
|
||||||
icon: panel.icon ?? "mdi:view-dashboard",
|
icon: panel.icon ?? "mdi:view-dashboard",
|
||||||
title:
|
title:
|
||||||
panel.url_path === hass.defaultPanel
|
panel.url_path === hass.sidebar.defaultPanel
|
||||||
? hass.localize("panel.states")
|
? hass.localize("panel.states")
|
||||||
: hass.localize(`panel.${panel.title}`) ||
|
: hass.localize(`panel.${panel.title}`) ||
|
||||||
panel.title ||
|
panel.title ||
|
||||||
|
@@ -49,6 +49,12 @@ const DEFAULT_ROW_RENDERER: ComboBoxLitRenderer<PickerComboBoxItem> = (
|
|||||||
</ha-combo-box-item>
|
</ha-combo-box-item>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
|
||||||
|
search: string,
|
||||||
|
filteredItems: T[],
|
||||||
|
allItems: T[]
|
||||||
|
) => T[];
|
||||||
|
|
||||||
@customElement("ha-picker-combo-box")
|
@customElement("ha-picker-combo-box")
|
||||||
export class HaPickerComboBox extends LitElement {
|
export class HaPickerComboBox extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -84,6 +90,9 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
@property({ attribute: "not-found-label", type: String })
|
@property({ attribute: "not-found-label", type: String })
|
||||||
public notFoundLabel?: string;
|
public notFoundLabel?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
|
||||||
|
|
||||||
@state() private _opened = false;
|
@state() private _opened = false;
|
||||||
|
|
||||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
@@ -237,6 +246,7 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
const fuse = new HaFuse(this._items, { shouldSort: false }, index);
|
const fuse = new HaFuse(this._items, { shouldSort: false }, index);
|
||||||
|
|
||||||
const results = fuse.multiTermsSearch(searchString);
|
const results = fuse.multiTermsSearch(searchString);
|
||||||
|
let filteredItems = this._items as PickerComboBoxItem[];
|
||||||
if (results) {
|
if (results) {
|
||||||
const items = results.map((result) => result.item);
|
const items = results.map((result) => result.item);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
@@ -246,10 +256,14 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
}
|
}
|
||||||
const additionalItems = this._getAdditionalItems(searchString);
|
const additionalItems = this._getAdditionalItems(searchString);
|
||||||
items.push(...additionalItems);
|
items.push(...additionalItems);
|
||||||
target.filteredItems = items;
|
filteredItems = items;
|
||||||
} else {
|
|
||||||
target.filteredItems = this._items;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.searchFn) {
|
||||||
|
filteredItems = this.searchFn(searchString, filteredItems, this._items);
|
||||||
|
}
|
||||||
|
|
||||||
|
target.filteredItems = filteredItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: string | undefined) {
|
private _setValue(value: string | undefined) {
|
||||||
|
@@ -88,6 +88,12 @@ export class HaPickerField extends LitElement {
|
|||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
css`
|
css`
|
||||||
|
ha-combo-box-item[disabled] {
|
||||||
|
background-color: var(
|
||||||
|
--mdc-text-field-disabled-fill-color,
|
||||||
|
whitesmoke
|
||||||
|
);
|
||||||
|
}
|
||||||
ha-combo-box-item {
|
ha-combo-box-item {
|
||||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -95,8 +101,8 @@ export class HaPickerField extends LitElement {
|
|||||||
border-end-start-radius: 0;
|
border-end-start-radius: 0;
|
||||||
--md-list-item-one-line-container-height: 56px;
|
--md-list-item-one-line-container-height: 56px;
|
||||||
--md-list-item-two-line-container-height: 56px;
|
--md-list-item-two-line-container-height: 56px;
|
||||||
--md-list-item-top-space: 8px;
|
--md-list-item-top-space: 0px;
|
||||||
--md-list-item-bottom-space: 8px;
|
--md-list-item-bottom-space: 0px;
|
||||||
--md-list-item-leading-space: 8px;
|
--md-list-item-leading-space: 8px;
|
||||||
--md-list-item-trailing-space: 8px;
|
--md-list-item-trailing-space: 8px;
|
||||||
--ha-md-list-item-gap: 8px;
|
--ha-md-list-item-gap: 8px;
|
||||||
@@ -106,6 +112,12 @@ export class HaPickerField extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Add Similar focus style as the text field */
|
/* Add Similar focus style as the text field */
|
||||||
|
ha-combo-box-item[disabled]:after {
|
||||||
|
background-color: var(
|
||||||
|
--mdc-text-field-disabled-line-color,
|
||||||
|
rgba(0, 0, 0, 0.42)
|
||||||
|
);
|
||||||
|
}
|
||||||
ha-combo-box-item:after {
|
ha-combo-box-item:after {
|
||||||
display: block;
|
display: block;
|
||||||
content: "";
|
content: "";
|
||||||
|
@@ -19,6 +19,8 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
|||||||
export class HaActionSelector extends SubscribeMixin(LitElement) {
|
export class HaActionSelector extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public selector!: ActionSelector;
|
@property({ attribute: false }) public selector!: ActionSelector;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: Action;
|
@property({ attribute: false }) public value?: Action;
|
||||||
@@ -66,6 +68,7 @@ export class HaActionSelector extends SubscribeMixin(LitElement) {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.actions=${this._actions(this.value)}
|
.actions=${this._actions(this.value)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
></ha-automation-action>
|
></ha-automation-action>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,8 @@ import type { HomeAssistant } from "../../types";
|
|||||||
export class HaConditionSelector extends LitElement {
|
export class HaConditionSelector extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public selector!: ConditionSelector;
|
@property({ attribute: false }) public selector!: ConditionSelector;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: Condition;
|
@property({ attribute: false }) public value?: Condition;
|
||||||
@@ -24,6 +26,7 @@ export class HaConditionSelector extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.conditions=${this.value || []}
|
.conditions=${this.value || []}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
></ha-automation-condition>
|
></ha-automation-condition>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -11,6 +11,8 @@ import type { HomeAssistant } from "../../types";
|
|||||||
export class HaTriggerSelector extends LitElement {
|
export class HaTriggerSelector extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public selector!: TriggerSelector;
|
@property({ attribute: false }) public selector!: TriggerSelector;
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: Trigger;
|
@property({ attribute: false }) public value?: Trigger;
|
||||||
@@ -33,6 +35,7 @@ export class HaTriggerSelector extends LitElement {
|
|||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.triggers=${this._triggers(this.value)}
|
.triggers=${this._triggers(this.value)}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
></ha-automation-trigger>
|
></ha-automation-trigger>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -69,6 +69,8 @@ const LEGACY_UI_SELECTORS = new Set(["ui-action", "ui-color"]);
|
|||||||
export class HaSelector extends LitElement {
|
export class HaSelector extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property() public name?: string;
|
@property() public name?: string;
|
||||||
|
|
||||||
@property({ attribute: false }) public selector!: Selector;
|
@property({ attribute: false }) public selector!: Selector;
|
||||||
@@ -127,6 +129,7 @@ export class HaSelector extends LitElement {
|
|||||||
return html`
|
return html`
|
||||||
${dynamicElement(`ha-selector-${this._type}`, {
|
${dynamicElement(`ha-selector-${this._type}`, {
|
||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
|
narrow: this.narrow,
|
||||||
name: this.name,
|
name: this.name,
|
||||||
selector: this._handleLegacySelector(this.selector),
|
selector: this._handleLegacySelector(this.selector),
|
||||||
value: this.value,
|
value: this.value,
|
||||||
|
@@ -85,8 +85,11 @@ export class HaServiceControl extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
|
@property({ attribute: "show-advanced", type: Boolean })
|
||||||
false;
|
public showAdvanced = false;
|
||||||
|
|
||||||
|
@property({ attribute: "show-service-id", type: Boolean })
|
||||||
|
public showServiceId = false;
|
||||||
|
|
||||||
@property({ attribute: "hide-picker", type: Boolean, reflect: true })
|
@property({ attribute: "hide-picker", type: Boolean, reflect: true })
|
||||||
public hidePicker = false;
|
public hidePicker = false;
|
||||||
@@ -435,6 +438,7 @@ export class HaServiceControl extends LitElement {
|
|||||||
.value=${this._value?.action}
|
.value=${this._value?.action}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
@value-changed=${this._serviceChanged}
|
@value-changed=${this._serviceChanged}
|
||||||
|
.showServiceId=${this.showServiceId}
|
||||||
></ha-service-picker>`}
|
></ha-service-picker>`}
|
||||||
${this.hideDescription
|
${this.hideDescription
|
||||||
? nothing
|
? nothing
|
||||||
|
@@ -1,15 +1,25 @@
|
|||||||
|
import { mdiRoomService } from "@mdi/js";
|
||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { html, LitElement } from "lit";
|
import { html, LitElement, nothing, type TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { isValidServiceId } from "../common/entity/valid_service_id";
|
||||||
import type { LocalizeFunc } from "../common/translations/localize";
|
import type { LocalizeFunc } from "../common/translations/localize";
|
||||||
import { domainToName } from "../data/integration";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./ha-combo-box";
|
|
||||||
import "./ha-combo-box-item";
|
|
||||||
import "./ha-service-icon";
|
|
||||||
import { getServiceIcons } from "../data/icons";
|
import { getServiceIcons } from "../data/icons";
|
||||||
|
import { domainToName } from "../data/integration";
|
||||||
|
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
|
import "./ha-combo-box-item";
|
||||||
|
import "./ha-generic-picker";
|
||||||
|
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||||
|
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||||
|
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||||
|
import "./ha-service-icon";
|
||||||
|
|
||||||
|
interface ServiceComboBoxItem extends PickerComboBoxItem {
|
||||||
|
domain_name?: string;
|
||||||
|
service_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@customElement("ha-service-picker")
|
@customElement("ha-service-picker")
|
||||||
class HaServicePicker extends LitElement {
|
class HaServicePicker extends LitElement {
|
||||||
@@ -17,66 +27,121 @@ class HaServicePicker extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
|
|
||||||
@state() private _filter?: string;
|
@property({ attribute: "show-service-id", type: Boolean })
|
||||||
|
public showServiceId = false;
|
||||||
|
|
||||||
protected willUpdate() {
|
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||||
if (!this.hasUpdated) {
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this._picker?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(props) {
|
||||||
|
super.firstUpdated(props);
|
||||||
this.hass.loadBackendTranslation("services");
|
this.hass.loadBackendTranslation("services");
|
||||||
getServiceIcons(this.hass);
|
getServiceIcons(this.hass);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
|
private _rowRenderer: ComboBoxLitRenderer<ServiceComboBoxItem> = (
|
||||||
(item) => html`
|
item,
|
||||||
<ha-combo-box-item type="button">
|
{ index }
|
||||||
|
) => html`
|
||||||
|
<ha-combo-box-item type="button" border-top .borderTop=${index !== 0}>
|
||||||
<ha-service-icon
|
<ha-service-icon
|
||||||
slot="start"
|
slot="start"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.service=${item.service}
|
.service=${item.id}
|
||||||
></ha-service-icon>
|
></ha-service-icon>
|
||||||
<span slot="headline">${item.name}</span>
|
<span slot="headline">${item.primary}</span>
|
||||||
<span slot="supporting-text"
|
<span slot="supporting-text">${item.secondary}</span>
|
||||||
>${item.name === item.service ? "" : item.service}</span
|
${item.service_id && this.showServiceId
|
||||||
>
|
? html`<span slot="supporting-text" class="code">
|
||||||
|
${item.service_id}
|
||||||
|
</span>`
|
||||||
|
: nothing}
|
||||||
|
${item.domain_name
|
||||||
|
? html`
|
||||||
|
<div slot="trailing-supporting-text" class="domain">
|
||||||
|
${item.domain_name}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</ha-combo-box-item>
|
</ha-combo-box-item>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
protected render() {
|
private _valueRenderer: PickerValueRenderer = (value) => {
|
||||||
|
const serviceId = value;
|
||||||
|
const [domain, service] = serviceId.split(".");
|
||||||
|
|
||||||
|
if (!this.hass.services[domain]?.[service]) {
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
|
||||||
.hass=${this.hass}
|
<span slot="headline">${value}</span>
|
||||||
.label=${this.hass.localize("ui.components.service-picker.action")}
|
|
||||||
.filteredItems=${this._filteredServices(
|
|
||||||
this.hass.localize,
|
|
||||||
this.hass.services,
|
|
||||||
this._filter
|
|
||||||
)}
|
|
||||||
.value=${this.value}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
.renderer=${this._rowRenderer}
|
|
||||||
item-value-path="service"
|
|
||||||
item-label-path="name"
|
|
||||||
allow-custom-value
|
|
||||||
@filter-changed=${this._filterChanged}
|
|
||||||
@value-changed=${this._valueChanged}
|
|
||||||
></ha-combo-box>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serviceName =
|
||||||
|
this.hass.localize(`component.${domain}.services.${service}.name`) ||
|
||||||
|
this.hass.services[domain][service].name ||
|
||||||
|
service;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-service-icon
|
||||||
|
slot="start"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.service=${serviceId}
|
||||||
|
></ha-service-icon>
|
||||||
|
<span slot="headline">${serviceName}</span>
|
||||||
|
${this.showServiceId
|
||||||
|
? html`<span slot="supporting-text" class="code">${serviceId}</span>`
|
||||||
|
: nothing}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const placeholder =
|
||||||
|
this.placeholder ??
|
||||||
|
this.hass.localize("ui.components.service-picker.action");
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-generic-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
allow-custom-value
|
||||||
|
.notFoundLabel=${this.hass.localize(
|
||||||
|
"ui.components.service-picker.no_match"
|
||||||
|
)}
|
||||||
|
.label=${this.label}
|
||||||
|
.placeholder=${placeholder}
|
||||||
|
.value=${this.value}
|
||||||
|
.getItems=${this._getItems}
|
||||||
|
.rowRenderer=${this._rowRenderer}
|
||||||
|
.valueRenderer=${this._valueRenderer}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
>
|
||||||
|
</ha-generic-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getItems = () =>
|
||||||
|
this._services(this.hass.localize, this.hass.services);
|
||||||
|
|
||||||
private _services = memoizeOne(
|
private _services = memoizeOne(
|
||||||
(
|
(
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
services: HomeAssistant["services"]
|
services: HomeAssistant["services"]
|
||||||
): {
|
): ServiceComboBoxItem[] => {
|
||||||
service: string;
|
|
||||||
name: string;
|
|
||||||
}[] => {
|
|
||||||
if (!services) {
|
if (!services) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const result: { service: string; name: string }[] = [];
|
const items: ServiceComboBoxItem[] = [];
|
||||||
|
|
||||||
Object.keys(services)
|
Object.keys(services)
|
||||||
.sort()
|
.sort()
|
||||||
@@ -84,56 +149,60 @@ class HaServicePicker extends LitElement {
|
|||||||
const services_keys = Object.keys(services[domain]).sort();
|
const services_keys = Object.keys(services[domain]).sort();
|
||||||
|
|
||||||
for (const service of services_keys) {
|
for (const service of services_keys) {
|
||||||
result.push({
|
const serviceId = `${domain}.${service}`;
|
||||||
service: `${domain}.${service}`,
|
const domainName = domainToName(localize, domain);
|
||||||
name: `${domainToName(localize, domain)}: ${
|
|
||||||
|
const name =
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
`component.${domain}.services.${service}.name`
|
`component.${domain}.services.${service}.name`
|
||||||
) ||
|
) ||
|
||||||
services[domain][service].name ||
|
services[domain][service].name ||
|
||||||
service
|
service;
|
||||||
}`,
|
|
||||||
|
const description =
|
||||||
|
this.hass.localize(
|
||||||
|
`component.${domain}.services.${service}.description`
|
||||||
|
) || services[domain][service].description;
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id: serviceId,
|
||||||
|
primary: name,
|
||||||
|
secondary: description,
|
||||||
|
domain_name: domainName,
|
||||||
|
service_id: serviceId,
|
||||||
|
search_labels: [serviceId, domainName, name, description].filter(
|
||||||
|
Boolean
|
||||||
|
),
|
||||||
|
sorting_label: serviceId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return items;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _filteredServices = memoizeOne(
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
(
|
ev.stopPropagation();
|
||||||
localize: LocalizeFunc,
|
const value = ev.detail.value;
|
||||||
services: HomeAssistant["services"],
|
|
||||||
filter?: string
|
|
||||||
) => {
|
|
||||||
if (!services) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const processedServices = this._services(localize, services);
|
|
||||||
|
|
||||||
if (!filter) {
|
if (!value) {
|
||||||
return processedServices;
|
this._setValue(undefined);
|
||||||
}
|
return;
|
||||||
const split_filter = filter.split(" ");
|
|
||||||
return processedServices.filter((service) => {
|
|
||||||
const lower_service_name = service.name.toLowerCase();
|
|
||||||
const lower_service = service.service.toLowerCase();
|
|
||||||
return split_filter.every(
|
|
||||||
(f) => lower_service_name.includes(f) || lower_service.includes(f)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private _filterChanged(ev: CustomEvent): void {
|
|
||||||
this._filter = ev.detail.value.toLowerCase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev) {
|
if (!isValidServiceId(value)) {
|
||||||
this.value = ev.detail.value;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value: string | undefined) {
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "change");
|
fireEvent(this, "change");
|
||||||
fireEvent(this, "value-changed", { value: this.value });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -50,6 +50,7 @@ import type { HaMdListItem } from "./ha-md-list-item";
|
|||||||
import "./ha-spinner";
|
import "./ha-spinner";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./user/ha-user-badge";
|
import "./user/ha-user-badge";
|
||||||
|
import { DEFAULT_PANEL } from "../data/panel";
|
||||||
|
|
||||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||||
|
|
||||||
@@ -140,9 +141,9 @@ const defaultPanelSorter = (
|
|||||||
export const computePanels = memoizeOne(
|
export const computePanels = memoizeOne(
|
||||||
(
|
(
|
||||||
panels: HomeAssistant["panels"],
|
panels: HomeAssistant["panels"],
|
||||||
defaultPanel: HomeAssistant["defaultPanel"],
|
defaultPanel: HomeAssistant["sidebar"]["defaultPanel"],
|
||||||
panelsOrder: string[],
|
panelsOrder: HomeAssistant["sidebar"]["panelOrder"],
|
||||||
hiddenPanels: string[],
|
hiddenPanels: HomeAssistant["sidebar"]["hiddenPanels"],
|
||||||
locale: HomeAssistant["locale"]
|
locale: HomeAssistant["locale"]
|
||||||
): [PanelInfo[], PanelInfo[]] => {
|
): [PanelInfo[], PanelInfo[]] => {
|
||||||
if (!panels) {
|
if (!panels) {
|
||||||
@@ -195,10 +196,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _issuesCount = 0;
|
@state() private _issuesCount = 0;
|
||||||
|
|
||||||
@state() private _panelOrder?: string[];
|
|
||||||
|
|
||||||
@state() private _hiddenPanels?: string[];
|
|
||||||
|
|
||||||
private _mouseLeaveTimeout?: number;
|
private _mouseLeaveTimeout?: number;
|
||||||
|
|
||||||
private _tooltipHideTimeout?: number;
|
private _tooltipHideTimeout?: number;
|
||||||
@@ -213,18 +210,32 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
this.hass.connection,
|
this.hass.connection,
|
||||||
"sidebar",
|
"sidebar",
|
||||||
({ value }) => {
|
({ value }) => {
|
||||||
this._panelOrder = value?.panelOrder;
|
let panelOrder = value?.panelOrder;
|
||||||
this._hiddenPanels = value?.hiddenPanels;
|
let hiddenPanels = value?.hiddenPanels;
|
||||||
|
let defaultPanel = value?.defaultPanel;
|
||||||
|
|
||||||
// fallback to old localStorage values
|
// fallback to old localStorage values
|
||||||
if (!this._panelOrder) {
|
if (!panelOrder) {
|
||||||
const storedOrder = localStorage.getItem("sidebarPanelOrder");
|
const storedOrder = localStorage.getItem("sidebarPanelOrder");
|
||||||
this._panelOrder = storedOrder ? JSON.parse(storedOrder) : [];
|
panelOrder = storedOrder ? JSON.parse(storedOrder) : [];
|
||||||
}
|
}
|
||||||
if (!this._hiddenPanels) {
|
if (!hiddenPanels) {
|
||||||
const storedHidden = localStorage.getItem("sidebarHiddenPanels");
|
const storedHidden = localStorage.getItem("sidebarHiddenPanels");
|
||||||
this._hiddenPanels = storedHidden ? JSON.parse(storedHidden) : [];
|
hiddenPanels = storedHidden ? JSON.parse(storedHidden) : [];
|
||||||
}
|
}
|
||||||
|
if (!defaultPanel) {
|
||||||
|
const storedDefault = localStorage.getItem("defaultPanel");
|
||||||
|
defaultPanel = storedDefault
|
||||||
|
? JSON.parse(storedDefault)
|
||||||
|
: DEFAULT_PANEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "hass-set-sidebar-data", {
|
||||||
|
...value,
|
||||||
|
defaultPanel: defaultPanel as string,
|
||||||
|
panelOrder: panelOrder as string[],
|
||||||
|
hiddenPanels: hiddenPanels as string[],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
subscribeNotifications(this.hass.connection, (notifications) => {
|
subscribeNotifications(this.hass.connection, (notifications) => {
|
||||||
@@ -275,8 +286,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
changedProps.has("_updatesCount") ||
|
changedProps.has("_updatesCount") ||
|
||||||
changedProps.has("_issuesCount") ||
|
changedProps.has("_issuesCount") ||
|
||||||
changedProps.has("_notifications") ||
|
changedProps.has("_notifications") ||
|
||||||
changedProps.has("_hiddenPanels") ||
|
(changedProps.has("hass") &&
|
||||||
changedProps.has("_panelOrder")
|
changedProps.get("hass")?.sidebar !== this.hass.sidebar)
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -295,7 +306,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
hass.localize !== oldHass.localize ||
|
hass.localize !== oldHass.localize ||
|
||||||
hass.locale !== oldHass.locale ||
|
hass.locale !== oldHass.locale ||
|
||||||
hass.states !== oldHass.states ||
|
hass.states !== oldHass.states ||
|
||||||
hass.defaultPanel !== oldHass.defaultPanel ||
|
hass.sidebar !== oldHass.sidebar ||
|
||||||
hass.connected !== oldHass.connected
|
hass.connected !== oldHass.connected
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -365,19 +376,19 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _renderAllPanels(selectedPanel: string) {
|
private _renderAllPanels(selectedPanel: string) {
|
||||||
if (!this._panelOrder || !this._hiddenPanels) {
|
if (!this.hass.sidebar.panelOrder || !this.hass.sidebar.hiddenPanels) {
|
||||||
return html`
|
return html`
|
||||||
<ha-fade-in .delay=${500}
|
<ha-fade-in .delay=${500}
|
||||||
><ha-spinner size="large"></ha-spinner
|
><ha-spinner size="small"></ha-spinner
|
||||||
></ha-fade-in>
|
></ha-fade-in>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [beforeSpacer, afterSpacer] = computePanels(
|
const [beforeSpacer, afterSpacer] = computePanels(
|
||||||
this.hass.panels,
|
this.hass.panels,
|
||||||
this.hass.defaultPanel,
|
this.hass.sidebar.defaultPanel,
|
||||||
this._panelOrder,
|
this.hass.sidebar.panelOrder,
|
||||||
this._hiddenPanels,
|
this.hass.sidebar.hiddenPanels,
|
||||||
this.hass.locale
|
this.hass.locale
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -402,11 +413,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
return panels.map((panel) =>
|
return panels.map((panel) =>
|
||||||
this._renderPanel(
|
this._renderPanel(
|
||||||
panel.url_path,
|
panel.url_path,
|
||||||
panel.url_path === this.hass.defaultPanel
|
panel.url_path === this.hass.sidebar.defaultPanel
|
||||||
? panel.title || this.hass.localize("panel.states")
|
? panel.title || this.hass.localize("panel.states")
|
||||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||||
panel.icon,
|
panel.icon,
|
||||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
panel.url_path === this.hass.sidebar.defaultPanel && !panel.icon
|
||||||
? PANEL_ICONS.lovelace
|
? PANEL_ICONS.lovelace
|
||||||
: panel.url_path in PANEL_ICONS
|
: panel.url_path in PANEL_ICONS
|
||||||
? PANEL_ICONS[panel.url_path]
|
? PANEL_ICONS[panel.url_path]
|
||||||
@@ -626,12 +637,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
this._tooltipHideTimeout = undefined;
|
this._tooltipHideTimeout = undefined;
|
||||||
}
|
}
|
||||||
const tooltip = this._tooltip;
|
const tooltip = this._tooltip;
|
||||||
const listbox = this.shadowRoot!.querySelector("ha-md-list")!;
|
const allListbox = this.shadowRoot!.querySelectorAll("ha-md-list")!;
|
||||||
let top = item.offsetTop + 11;
|
const listbox = [...allListbox].find((lb) => lb.contains(item));
|
||||||
if (listbox.contains(item)) {
|
|
||||||
top += listbox.offsetTop;
|
const top =
|
||||||
top -= listbox.scrollTop;
|
item.offsetTop +
|
||||||
}
|
11 +
|
||||||
|
(listbox?.offsetTop ?? 0) -
|
||||||
|
(listbox?.scrollTop ?? 0);
|
||||||
|
|
||||||
tooltip.innerText = (
|
tooltip.innerText = (
|
||||||
item.querySelector(".item-text") as HTMLElement
|
item.querySelector(".item-text") as HTMLElement
|
||||||
).innerText;
|
).innerText;
|
||||||
|
@@ -12,6 +12,8 @@ class HaEntityMarker extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: "entity-name" }) public entityName?: string;
|
@property({ attribute: "entity-name" }) public entityName?: string;
|
||||||
|
|
||||||
|
@property({ attribute: "entity-unit" }) public entityUnit?: string;
|
||||||
|
|
||||||
@property({ attribute: "entity-picture" }) public entityPicture?: string;
|
@property({ attribute: "entity-picture" }) public entityPicture?: string;
|
||||||
|
|
||||||
@property({ attribute: "entity-color" }) public entityColor?: string;
|
@property({ attribute: "entity-color" }) public entityColor?: string;
|
||||||
@@ -37,7 +39,16 @@ class HaEntityMarker extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.stateObj=${this.hass?.states[this.entityId]}
|
.stateObj=${this.hass?.states[this.entityId]}
|
||||||
></ha-state-icon>`
|
></ha-state-icon>`
|
||||||
: this.entityName}
|
: !this.entityUnit
|
||||||
|
? this.entityName
|
||||||
|
: html`
|
||||||
|
${this.entityName}
|
||||||
|
<span
|
||||||
|
class="unit"
|
||||||
|
style="display: ${this.entityUnit ? "initial" : "none"}"
|
||||||
|
>${this.entityUnit}</span
|
||||||
|
>
|
||||||
|
`}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -72,6 +83,9 @@ class HaEntityMarker extends LitElement {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.unit {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -56,6 +56,7 @@ export interface HaMapEntity {
|
|||||||
color: string;
|
color: string;
|
||||||
label_mode?: "name" | "state" | "attribute" | "icon";
|
label_mode?: "name" | "state" | "attribute" | "icon";
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
|
unit?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
}
|
}
|
||||||
@@ -549,6 +550,12 @@ export class HaMap extends ReactiveElement {
|
|||||||
typeof entity !== "string" && entity.label_mode === "icon";
|
typeof entity !== "string" && entity.label_mode === "icon";
|
||||||
entityMarker.entityId = getEntityId(entity);
|
entityMarker.entityId = getEntityId(entity);
|
||||||
entityMarker.entityName = entityName;
|
entityMarker.entityName = entityName;
|
||||||
|
entityMarker.entityUnit =
|
||||||
|
typeof entity !== "string" &&
|
||||||
|
entity.unit &&
|
||||||
|
entity.label_mode === "attribute"
|
||||||
|
? entity.unit
|
||||||
|
: "";
|
||||||
entityMarker.entityPicture =
|
entityMarker.entityPicture =
|
||||||
entityPicture && (typeof entity === "string" || !entity.label_mode)
|
entityPicture && (typeof entity === "string" || !entity.label_mode)
|
||||||
? this.hass.hassUrl(entityPicture)
|
? this.hass.hassUrl(entityPicture)
|
||||||
|
@@ -8,6 +8,7 @@ export interface CoreFrontendUserData {
|
|||||||
export interface SidebarFrontendUserData {
|
export interface SidebarFrontendUserData {
|
||||||
panelOrder: string[];
|
panelOrder: string[];
|
||||||
hiddenPanels: string[];
|
hiddenPanels: string[];
|
||||||
|
defaultPanel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -640,6 +640,12 @@ export const mergeHistoryResults = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const item of ltsResult.line) {
|
for (const item of ltsResult.line) {
|
||||||
|
if (item.unit === BLANK_UNIT) {
|
||||||
|
// disabled entities have no unit, so we need to find the unit from the history result
|
||||||
|
item.unit =
|
||||||
|
historyResult.line.find((line) => line.identifier === item.identifier)
|
||||||
|
?.unit ?? BLANK_UNIT;
|
||||||
|
}
|
||||||
const key = computeGroupKey(
|
const key = computeGroupKey(
|
||||||
item.unit,
|
item.unit,
|
||||||
item.device_class,
|
item.device_class,
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
import type { HomeAssistant, PanelInfo } from "../types";
|
import type { HomeAssistant, PanelInfo } from "../types";
|
||||||
|
|
||||||
/** Panel to show when no panel is picked. */
|
/** Panel to show when no panel is picked. */
|
||||||
@@ -10,16 +9,9 @@ export const getStorageDefaultPanelUrlPath = (): string => {
|
|||||||
return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL;
|
return defaultPanel ? JSON.parse(defaultPanel) : DEFAULT_PANEL;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setDefaultPanel = (
|
|
||||||
element: HTMLElement,
|
|
||||||
urlPath: string
|
|
||||||
): void => {
|
|
||||||
fireEvent(element, "hass-default-panel", { defaultPanel: urlPath });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
|
export const getDefaultPanel = (hass: HomeAssistant): PanelInfo =>
|
||||||
hass.panels[hass.defaultPanel]
|
hass.panels[hass.sidebar.defaultPanel]
|
||||||
? hass.panels[hass.defaultPanel]
|
? hass.panels[hass.sidebar.defaultPanel]
|
||||||
: hass.panels[DEFAULT_PANEL];
|
: hass.panels[DEFAULT_PANEL];
|
||||||
|
|
||||||
export const getPanelNameTranslationKey = (panel: PanelInfo) => {
|
export const getPanelNameTranslationKey = (panel: PanelInfo) => {
|
||||||
|
@@ -6,6 +6,7 @@ const HAS_CUSTOM_PREVIEW = ["generic_camera", "template"];
|
|||||||
export interface GenericPreview {
|
export interface GenericPreview {
|
||||||
state: string;
|
state: string;
|
||||||
attributes: Record<string, any>;
|
attributes: Record<string, any>;
|
||||||
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const subscribePreviewGeneric = (
|
export const subscribePreviewGeneric = (
|
||||||
|
@@ -349,6 +349,7 @@ class DataEntryFlowDialog extends LitElement {
|
|||||||
${this._step.type === "form"
|
${this._step.type === "form"
|
||||||
? html`
|
? html`
|
||||||
<step-flow-form
|
<step-flow-form
|
||||||
|
narrow
|
||||||
.flowConfig=${this._params.flowConfig}
|
.flowConfig=${this._params.flowConfig}
|
||||||
.step=${this._step}
|
.step=${this._step}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
@@ -58,6 +58,11 @@ export class FlowPreviewGeneric extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _setPreview = (preview: GenericPreview) => {
|
private _setPreview = (preview: GenericPreview) => {
|
||||||
|
if (preview.error) {
|
||||||
|
this._error = preview.error;
|
||||||
|
this._preview = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
this._preview = {
|
this._preview = {
|
||||||
entity_id: `${this.stepId}.___flow_preview___`,
|
entity_id: `${this.stepId}.___flow_preview___`,
|
||||||
@@ -80,6 +85,7 @@ export class FlowPreviewGeneric extends LitElement {
|
|||||||
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
if (this.flowType !== "config_flow" && this.flowType !== "options_flow") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._error = undefined;
|
||||||
try {
|
try {
|
||||||
this._unsub = subscribePreviewGeneric(
|
this._unsub = subscribePreviewGeneric(
|
||||||
this.hass,
|
this.hass,
|
||||||
@@ -89,6 +95,7 @@ export class FlowPreviewGeneric extends LitElement {
|
|||||||
this.stepData,
|
this.stepData,
|
||||||
this._setPreview
|
this._setPreview
|
||||||
);
|
);
|
||||||
|
await this._unsub;
|
||||||
fireEvent(this, "set-flow-errors", { errors: {} });
|
fireEvent(this, "set-flow-errors", { errors: {} });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (typeof err.message === "string") {
|
if (typeof err.message === "string") {
|
||||||
|
@@ -235,11 +235,15 @@ class StepFlowCreateEntry extends LitElement {
|
|||||||
|
|
||||||
fireEvent(this, "flow-update", { step: undefined });
|
fireEvent(this, "flow-update", { step: undefined });
|
||||||
if (this.step.result && this.navigateToResult) {
|
if (this.step.result && this.navigateToResult) {
|
||||||
|
if (this.devices.length === 1) {
|
||||||
|
navigate(`/config/devices/device/${this.devices[0].id}`);
|
||||||
|
} else {
|
||||||
navigate(
|
navigate(
|
||||||
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
|
`/config/integrations/integration/${this.step.result.domain}#config_entry=${this.step.result.entry_id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async _areaPicked(ev: CustomEvent) {
|
private async _areaPicked(ev: CustomEvent) {
|
||||||
const picker = ev.currentTarget as any;
|
const picker = ev.currentTarget as any;
|
||||||
|
@@ -27,6 +27,8 @@ import { configFlowContentStyles } from "./styles";
|
|||||||
class StepFlowForm extends LitElement {
|
class StepFlowForm extends LitElement {
|
||||||
@property({ attribute: false }) public flowConfig!: FlowConfig;
|
@property({ attribute: false }) public flowConfig!: FlowConfig;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public step!: DataEntryFlowStepForm;
|
@property({ attribute: false }) public step!: DataEntryFlowStepForm;
|
||||||
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -63,6 +65,7 @@ class StepFlowForm extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
<ha-form
|
<ha-form
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
.data=${stepData}
|
.data=${stepData}
|
||||||
.disabled=${this._loading}
|
.disabled=${this._loading}
|
||||||
@value-changed=${this._stepDataChanged}
|
@value-changed=${this._stepDataChanged}
|
||||||
|
@@ -45,8 +45,7 @@ class MoreInfoCamera extends LitElement {
|
|||||||
<ha-progress-button
|
<ha-progress-button
|
||||||
@click=${this._downloadSnapshot}
|
@click=${this._downloadSnapshot}
|
||||||
.progress=${this._waiting}
|
.progress=${this._waiting}
|
||||||
.disabled=${this.stateObj.state === UNAVAILABLE ||
|
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||||
this.stateObj.state === "idle"}
|
|
||||||
>
|
>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.dialogs.more_info_control.camera.download_snapshot"
|
"ui.dialogs.more_info_control.camera.download_snapshot"
|
||||||
|
@@ -96,7 +96,7 @@ class DialogEditSidebar extends LitElement {
|
|||||||
|
|
||||||
const [beforeSpacer, afterSpacer] = computePanels(
|
const [beforeSpacer, afterSpacer] = computePanels(
|
||||||
this.hass.panels,
|
this.hass.panels,
|
||||||
this.hass.defaultPanel,
|
this.hass.sidebar.defaultPanel,
|
||||||
this._order,
|
this._order,
|
||||||
this._hidden,
|
this._hidden,
|
||||||
this.hass.locale
|
this.hass.locale
|
||||||
@@ -109,12 +109,12 @@ class DialogEditSidebar extends LitElement {
|
|||||||
].map((panel) => ({
|
].map((panel) => ({
|
||||||
value: panel.url_path,
|
value: panel.url_path,
|
||||||
label:
|
label:
|
||||||
panel.url_path === this.hass.defaultPanel
|
panel.url_path === this.hass.sidebar.defaultPanel
|
||||||
? panel.title || this.hass.localize("panel.states")
|
? panel.title || this.hass.localize("panel.states")
|
||||||
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
|
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
|
||||||
icon: panel.icon || undefined,
|
icon: panel.icon || undefined,
|
||||||
iconPath:
|
iconPath:
|
||||||
panel.url_path === this.hass.defaultPanel && !panel.icon
|
panel.url_path === this.hass.sidebar.defaultPanel && !panel.icon
|
||||||
? PANEL_ICONS.lovelace
|
? PANEL_ICONS.lovelace
|
||||||
: panel.url_path in PANEL_ICONS
|
: panel.url_path in PANEL_ICONS
|
||||||
? PANEL_ICONS[panel.url_path]
|
? PANEL_ICONS[panel.url_path]
|
||||||
@@ -195,6 +195,7 @@ class DialogEditSidebar extends LitElement {
|
|||||||
await saveFrontendUserData(this.hass.connection, "sidebar", {
|
await saveFrontendUserData(this.hass.connection, "sidebar", {
|
||||||
panelOrder: this._order!,
|
panelOrder: this._order!,
|
||||||
hiddenPanels: this._hidden!,
|
hiddenPanels: this._hidden!,
|
||||||
|
defaultPanel: this.hass.sidebar.defaultPanel,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this._error = err.message || err;
|
this._error = err.message || err;
|
||||||
|
@@ -497,9 +497,9 @@ export class HAFullCalendar extends LitElement {
|
|||||||
|
|
||||||
ha-fab {
|
ha-fab {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 32px;
|
bottom: 16px;
|
||||||
right: 32px;
|
right: 16px;
|
||||||
inset-inline-end: 32px;
|
inset-inline-end: 16px;
|
||||||
inset-inline-start: initial;
|
inset-inline-start: initial;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
|
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
|
||||||
import type { IFuseOptions } from "fuse.js";
|
|
||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { LitElement, css, html, nothing } from "lit";
|
||||||
@@ -46,6 +45,7 @@ import { haStyle, haStyleDialog } from "../../../resources/styles";
|
|||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||||
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
||||||
|
import { HaFuse } from "../../../resources/fuse";
|
||||||
|
|
||||||
const TYPES = {
|
const TYPES = {
|
||||||
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
|
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
|
||||||
@@ -175,6 +175,40 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
|||||||
type: AddAutomationElementDialogParams["type"],
|
type: AddAutomationElementDialogParams["type"],
|
||||||
group: string | undefined,
|
group: string | undefined,
|
||||||
filter: string,
|
filter: string,
|
||||||
|
domains: Set<string> | undefined,
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
services: HomeAssistant["services"],
|
||||||
|
manifests?: DomainManifestLookup
|
||||||
|
): ListItem[] => {
|
||||||
|
const items = this._items(type, group, localize, services, manifests);
|
||||||
|
|
||||||
|
const index = this._fuseIndex(items);
|
||||||
|
|
||||||
|
const fuse = new HaFuse(
|
||||||
|
items,
|
||||||
|
{ ignoreLocation: true, includeScore: true },
|
||||||
|
index
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = fuse.multiTermsSearch(filter);
|
||||||
|
if (results) {
|
||||||
|
return results.map((result) => result.item);
|
||||||
|
}
|
||||||
|
return this._getGroupItems(
|
||||||
|
type,
|
||||||
|
group,
|
||||||
|
domains,
|
||||||
|
localize,
|
||||||
|
services,
|
||||||
|
manifests
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _items = memoizeOne(
|
||||||
|
(
|
||||||
|
type: AddAutomationElementDialogParams["type"],
|
||||||
|
group: string | undefined,
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
services: HomeAssistant["services"],
|
services: HomeAssistant["services"],
|
||||||
manifests?: DomainManifestLookup
|
manifests?: DomainManifestLookup
|
||||||
@@ -189,24 +223,17 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const items = flattenGroups(groups).flat();
|
const items = flattenGroups(groups).flat();
|
||||||
|
|
||||||
if (type === "action") {
|
if (type === "action") {
|
||||||
items.push(...this._services(localize, services, manifests, group));
|
items.push(...this._services(localize, services, manifests, group));
|
||||||
}
|
}
|
||||||
|
return items;
|
||||||
const options: IFuseOptions<ListItem> = {
|
|
||||||
keys: ["key", "name", "description"],
|
|
||||||
isCaseSensitive: false,
|
|
||||||
ignoreLocation: true,
|
|
||||||
minMatchCharLength: Math.min(filter.length, 2),
|
|
||||||
threshold: 0.2,
|
|
||||||
ignoreDiacritics: true,
|
|
||||||
};
|
|
||||||
const fuse = new Fuse(items, options);
|
|
||||||
return fuse.search(filter).map((result) => result.item);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _fuseIndex = memoizeOne((items: ListItem[]) =>
|
||||||
|
Fuse.createIndex(["key", "name", "description"], items)
|
||||||
|
);
|
||||||
|
|
||||||
private _getGroupItems = memoizeOne(
|
private _getGroupItems = memoizeOne(
|
||||||
(
|
(
|
||||||
type: AddAutomationElementDialogParams["type"],
|
type: AddAutomationElementDialogParams["type"],
|
||||||
@@ -449,6 +476,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
|||||||
this._params.type,
|
this._params.type,
|
||||||
this._group,
|
this._group,
|
||||||
this._filter,
|
this._filter,
|
||||||
|
this._domains,
|
||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.hass.services,
|
this.hass.services,
|
||||||
this._manifests
|
this._manifests
|
||||||
|
@@ -233,6 +233,8 @@ export class CloudLogin extends LitElement {
|
|||||||
text: this.hass.localize(
|
text: this.hass.localize(
|
||||||
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
"ui.panel.config.cloud.login.cloud_pipeline_text"
|
||||||
),
|
),
|
||||||
|
confirmText: this.hass.localize("ui.common.yes"),
|
||||||
|
dismissText: this.hass.localize("ui.common.no"),
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
|
setAssistPipelinePreferred(this.hass, result.cloud_pipeline);
|
||||||
|
@@ -4,6 +4,7 @@ import {
|
|||||||
mdiDeleteForever,
|
mdiDeleteForever,
|
||||||
mdiHospitalBox,
|
mdiHospitalBox,
|
||||||
mdiInformation,
|
mdiInformation,
|
||||||
|
mdiPlus,
|
||||||
mdiUpload,
|
mdiUpload,
|
||||||
mdiWrench,
|
mdiWrench,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
fetchZwaveIntegrationSettings,
|
fetchZwaveIntegrationSettings,
|
||||||
fetchZwaveIsAnyOTAFirmwareUpdateInProgress,
|
fetchZwaveIsAnyOTAFirmwareUpdateInProgress,
|
||||||
fetchZwaveIsNodeFirmwareUpdateInProgress,
|
fetchZwaveIsNodeFirmwareUpdateInProgress,
|
||||||
|
fetchZwaveNetworkStatus,
|
||||||
fetchZwaveNodeStatus,
|
fetchZwaveNodeStatus,
|
||||||
} from "../../../../../../data/zwave_js";
|
} from "../../../../../../data/zwave_js";
|
||||||
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
|
||||||
@@ -24,6 +26,7 @@ import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/inte
|
|||||||
import { showZWaveJSUpdateFirmwareNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-update-firmware-node";
|
import { showZWaveJSUpdateFirmwareNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-update-firmware-node";
|
||||||
import type { DeviceAction } from "../../../ha-config-device-page";
|
import type { DeviceAction } from "../../../ha-config-device-page";
|
||||||
import { showZWaveJSHardResetControllerDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-hard-reset-controller";
|
import { showZWaveJSHardResetControllerDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-hard-reset-controller";
|
||||||
|
import { showZWaveJSAddNodeDialog } from "../../../../integrations/integration-panels/zwave_js/add-node/show-dialog-zwave_js-add-node";
|
||||||
|
|
||||||
export const getZwaveDeviceActions = async (
|
export const getZwaveDeviceActions = async (
|
||||||
el: HTMLElement,
|
el: HTMLElement,
|
||||||
@@ -160,6 +163,19 @@ export const getZwaveDeviceActions = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nodeStatus.is_controller_node) {
|
if (nodeStatus.is_controller_node) {
|
||||||
|
const networkStatus = await fetchZwaveNetworkStatus(hass, {
|
||||||
|
entry_id: entryId,
|
||||||
|
});
|
||||||
|
actions.unshift({
|
||||||
|
label: hass.localize("ui.panel.config.zwave_js.common.add_node"),
|
||||||
|
icon: mdiPlus,
|
||||||
|
action: async () => {
|
||||||
|
showZWaveJSAddNodeDialog(el, {
|
||||||
|
entry_id: entryId,
|
||||||
|
longRangeSupported: networkStatus.controller?.supports_long_range,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
actions.push({
|
actions.push({
|
||||||
label: hass.localize(
|
label: hass.localize(
|
||||||
"ui.panel.config.zwave_js.device_info.hard_reset_controller"
|
"ui.panel.config.zwave_js.device_info.hard_reset_controller"
|
||||||
|
@@ -163,6 +163,7 @@ export class HaIntegrationCard extends LitElement {
|
|||||||
: "custom"}"
|
: "custom"}"
|
||||||
>
|
>
|
||||||
<ha-tooltip
|
<ha-tooltip
|
||||||
|
hoist
|
||||||
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
||||||
.content=${this.hass.localize(
|
.content=${this.hass.localize(
|
||||||
this.manifest.overwrites_built_in
|
this.manifest.overwrites_built_in
|
||||||
@@ -177,6 +178,7 @@ export class HaIntegrationCard extends LitElement {
|
|||||||
${this.manifest && this.manifest.iot_class?.startsWith("cloud_")
|
${this.manifest && this.manifest.iot_class?.startsWith("cloud_")
|
||||||
? html`<div class="icon cloud">
|
? html`<div class="icon cloud">
|
||||||
<ha-tooltip
|
<ha-tooltip
|
||||||
|
hoist
|
||||||
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
||||||
.content=${this.hass.localize(
|
.content=${this.hass.localize(
|
||||||
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
"ui.panel.config.integrations.config_entry.depends_on_cloud"
|
||||||
@@ -191,6 +193,7 @@ export class HaIntegrationCard extends LitElement {
|
|||||||
!this.items.every((itm) => itm.source === "system")
|
!this.items.every((itm) => itm.source === "system")
|
||||||
? html`<div class="icon yaml">
|
? html`<div class="icon yaml">
|
||||||
<ha-tooltip
|
<ha-tooltip
|
||||||
|
hoist
|
||||||
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
.placement=${computeRTL(this.hass) ? "right" : "left"}
|
||||||
.content=${this.hass.localize(
|
.content=${this.hass.localize(
|
||||||
"ui.panel.config.integrations.config_entry.no_config_flow"
|
"ui.panel.config.integrations.config_entry.no_config_flow"
|
||||||
|
@@ -133,8 +133,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
|||||||
if (ERROR_STATES.includes(this._configEntry.state)) {
|
if (ERROR_STATES.includes(this._configEntry.state)) {
|
||||||
return this._renderErrorScreen();
|
return this._renderErrorScreen();
|
||||||
}
|
}
|
||||||
|
const provisioningDevices =
|
||||||
|
this._provisioningEntries?.filter((entry) => !entry.nodeId).length ?? 0;
|
||||||
const notReadyDevices =
|
const notReadyDevices =
|
||||||
this._network?.controller.nodes.filter((node) => !node.ready).length ?? 0;
|
(this._network?.controller.nodes.filter((node) => !node.ready).length ??
|
||||||
|
0) + provisioningDevices;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
@@ -182,7 +185,9 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
|
|||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
`ui.panel.config.zwave_js.dashboard.devices`,
|
`ui.panel.config.zwave_js.dashboard.devices`,
|
||||||
{
|
{
|
||||||
count: this._network.controller.nodes.length,
|
count:
|
||||||
|
this._network.controller.nodes.length +
|
||||||
|
provisioningDevices,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
${notReadyDevices > 0
|
${notReadyDevices > 0
|
||||||
|
@@ -13,10 +13,11 @@ import type {
|
|||||||
LovelaceDashboardCreateParams,
|
LovelaceDashboardCreateParams,
|
||||||
LovelaceDashboardMutableParams,
|
LovelaceDashboardMutableParams,
|
||||||
} from "../../../../data/lovelace/dashboard";
|
} from "../../../../data/lovelace/dashboard";
|
||||||
import { DEFAULT_PANEL, setDefaultPanel } from "../../../../data/panel";
|
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||||
import { haStyleDialog } from "../../../../resources/styles";
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||||
|
import { saveFrontendUserData } from "../../../../data/frontend";
|
||||||
|
|
||||||
@customElement("dialog-lovelace-dashboard-detail")
|
@customElement("dialog-lovelace-dashboard-detail")
|
||||||
export class DialogLovelaceDashboardDetail extends LitElement {
|
export class DialogLovelaceDashboardDetail extends LitElement {
|
||||||
@@ -59,7 +60,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
|||||||
if (!this._params || !this._data) {
|
if (!this._params || !this._data) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const defaultPanelUrlPath = this.hass.defaultPanel;
|
const defaultPanelUrlPath = this.hass.sidebar.defaultPanel;
|
||||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -249,15 +250,17 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleDefault() {
|
private async _toggleDefault() {
|
||||||
const urlPath = this._params?.urlPath;
|
const urlPath = this._params?.urlPath;
|
||||||
if (!urlPath) {
|
if (!urlPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDefaultPanel(
|
await saveFrontendUserData(this.hass!.connection, "sidebar", {
|
||||||
this,
|
panelOrder: this.hass!.sidebar.panelOrder,
|
||||||
urlPath === this.hass.defaultPanel ? DEFAULT_PANEL : urlPath
|
hiddenPanels: this.hass!.sidebar.hiddenPanels,
|
||||||
);
|
defaultPanel:
|
||||||
|
urlPath === this.hass.sidebar.defaultPanel ? DEFAULT_PANEL : urlPath,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _updateDashboard() {
|
private async _updateDashboard() {
|
||||||
|
@@ -255,7 +255,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
const defaultMode = (
|
const defaultMode = (
|
||||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||||
).mode;
|
).mode;
|
||||||
const defaultUrlPath = this.hass.defaultPanel;
|
const defaultUrlPath = this.hass.sidebar.defaultPanel;
|
||||||
const isDefault = defaultUrlPath === "lovelace";
|
const isDefault = defaultUrlPath === "lovelace";
|
||||||
const result: DataTableItem[] = [
|
const result: DataTableItem[] = [
|
||||||
{
|
{
|
||||||
|
@@ -142,6 +142,7 @@ class HaPanelDevAction extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._serviceData?.action}
|
.value=${this._serviceData?.action}
|
||||||
@value-changed=${this._serviceChanged}
|
@value-changed=${this._serviceChanged}
|
||||||
|
show-service-id
|
||||||
></ha-service-picker>
|
></ha-service-picker>
|
||||||
<ha-yaml-editor
|
<ha-yaml-editor
|
||||||
id="yaml-editor"
|
id="yaml-editor"
|
||||||
@@ -156,6 +157,7 @@ class HaPanelDevAction extends LitElement {
|
|||||||
.value=${this._serviceData}
|
.value=${this._serviceData}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
show-advanced
|
show-advanced
|
||||||
|
show-service-id
|
||||||
@value-changed=${this._serviceDataChanged}
|
@value-changed=${this._serviceDataChanged}
|
||||||
class="card-content"
|
class="card-content"
|
||||||
></ha-service-control>
|
></ha-service-control>
|
||||||
|
@@ -130,6 +130,7 @@ class HaPanelDevState extends LitElement {
|
|||||||
.value=${this._entityId}
|
.value=${this._entityId}
|
||||||
@value-changed=${this._entityIdChanged}
|
@value-changed=${this._entityIdChanged}
|
||||||
allow-custom-entity
|
allow-custom-entity
|
||||||
|
show-entity-id
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
${this._entityId
|
${this._entityId
|
||||||
? html`
|
? html`
|
||||||
|
@@ -6,6 +6,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import type { BarSeriesOption } from "echarts/charts";
|
import type { BarSeriesOption } from "echarts/charts";
|
||||||
|
import type { LegendComponentOption } from "echarts/components";
|
||||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||||
import { getEnergyColor } from "./common/color";
|
import { getEnergyColor } from "./common/color";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
@@ -54,6 +55,8 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
|
|
||||||
@state() private _data?: EnergyData;
|
@state() private _data?: EnergyData;
|
||||||
|
|
||||||
|
@state() private _legendData?: LegendComponentOption["data"];
|
||||||
|
|
||||||
@state() private _start = startOfToday();
|
@state() private _start = startOfToday();
|
||||||
|
|
||||||
@state() private _end = endOfToday();
|
@state() private _end = endOfToday();
|
||||||
@@ -185,6 +188,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
legend: {
|
legend: {
|
||||||
show: true,
|
show: true,
|
||||||
type: "custom",
|
type: "custom",
|
||||||
|
data: this._legendData,
|
||||||
selected: this._hiddenStats.reduce((acc, stat) => {
|
selected: this._hiddenStats.reduce((acc, stat) => {
|
||||||
acc[stat] = false;
|
acc[stat] = false;
|
||||||
return acc;
|
return acc;
|
||||||
@@ -310,6 +314,13 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
);
|
);
|
||||||
|
|
||||||
datasets.push(...processedData);
|
datasets.push(...processedData);
|
||||||
|
this._legendData = processedData.map((d) => ({
|
||||||
|
name: d.name as string,
|
||||||
|
itemStyle: {
|
||||||
|
color: d.color as string,
|
||||||
|
borderColor: d.itemStyle?.borderColor as string,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
if (showUntracked) {
|
if (showUntracked) {
|
||||||
const untrackedData = this._processUntracked(
|
const untrackedData = this._processUntracked(
|
||||||
@@ -319,6 +330,13 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
datasets.push(untrackedData);
|
datasets.push(untrackedData);
|
||||||
|
this._legendData.push({
|
||||||
|
name: untrackedData.name as string,
|
||||||
|
itemStyle: {
|
||||||
|
color: untrackedData.color as string,
|
||||||
|
borderColor: untrackedData.itemStyle?.borderColor as string,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fillDataGapsAndRoundCaps(datasets);
|
fillDataGapsAndRoundCaps(datasets);
|
||||||
|
@@ -384,7 +384,10 @@ export class HuiAreaCard
|
|||||||
areaSensorEntityId = area.humidity_entity_id;
|
areaSensorEntityId = area.humidity_entity_id;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const areaEntity = areaSensorEntityId
|
const areaEntity =
|
||||||
|
areaSensorEntityId &&
|
||||||
|
this.hass.states[areaSensorEntityId] &&
|
||||||
|
!isUnavailableState(this.hass.states[areaSensorEntityId].state)
|
||||||
? this.hass.states[areaSensorEntityId]
|
? this.hass.states[areaSensorEntityId]
|
||||||
: undefined;
|
: undefined;
|
||||||
if (
|
if (
|
||||||
@@ -393,6 +396,10 @@ export class HuiAreaCard
|
|||||||
(entity) => entity.attributes.device_class === deviceClass
|
(entity) => entity.attributes.device_class === deviceClass
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
let value = areaEntity
|
||||||
|
? this.hass.formatEntityState(areaEntity)
|
||||||
|
: this._average(domain, deviceClass);
|
||||||
|
if (!value) value = "—";
|
||||||
sensors.push(html`
|
sensors.push(html`
|
||||||
<div class="sensor">
|
<div class="sensor">
|
||||||
<ha-domain-icon
|
<ha-domain-icon
|
||||||
@@ -400,9 +407,7 @@ export class HuiAreaCard
|
|||||||
.domain=${domain}
|
.domain=${domain}
|
||||||
.deviceClass=${deviceClass}
|
.deviceClass=${deviceClass}
|
||||||
></ha-domain-icon>
|
></ha-domain-icon>
|
||||||
${areaEntity
|
${value}
|
||||||
? this.hass.formatEntityState(areaEntity)
|
|
||||||
: this._average(domain, deviceClass)}
|
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
@@ -224,19 +224,19 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
|||||||
filter: colored ? stateColorBrightness(stateObj) : undefined,
|
filter: colored ? stateColorBrightness(stateObj) : undefined,
|
||||||
height: this._config.icon_height
|
height: this._config.icon_height
|
||||||
? this._config.icon_height
|
? this._config.icon_height
|
||||||
: "",
|
: undefined,
|
||||||
})}
|
})}
|
||||||
></ha-state-icon>
|
></ha-state-icon>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
${this._config.show_name
|
${this._config.show_name
|
||||||
? html`<span tabindex="-1" .title=${name}>${name}</span>`
|
? html`<span tabindex="-1" .title=${name}>${name}</span>`
|
||||||
: ""}
|
: nothing}
|
||||||
${this._config.show_state && stateObj
|
${this._config.show_state && stateObj
|
||||||
? html`<span class="state">
|
? html`<span class="state">
|
||||||
${this.hass.formatEntityState(stateObj)}
|
${this.hass.formatEntityState(stateObj)}
|
||||||
</span>`
|
</span>`
|
||||||
: ""}
|
: nothing}
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -282,7 +282,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 4% 0;
|
padding: 4% 0;
|
||||||
font-size: 16.8px;
|
font-size: var(--ha-font-size-l);
|
||||||
|
line-height: var(--ha-line-height-condensed);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@@ -301,7 +301,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
line-height: var(--ha-line-height-expanded);
|
line-height: var(--ha-line-height-condensed);
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
|
@@ -45,6 +45,7 @@ export const DEFAULT_ZOOM = 14;
|
|||||||
interface MapEntityConfig extends EntityConfig {
|
interface MapEntityConfig extends EntityConfig {
|
||||||
label_mode?: "state" | "attribute" | "name";
|
label_mode?: "state" | "attribute" | "name";
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
|
unit?: string;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +53,7 @@ interface GeoEntity {
|
|||||||
entity_id: string;
|
entity_id: string;
|
||||||
label_mode?: "state" | "attribute" | "name" | "icon";
|
label_mode?: "state" | "attribute" | "name" | "icon";
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
|
unit?: string;
|
||||||
focus: boolean;
|
focus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,6 +432,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
entity_id: stateObj.entity_id,
|
entity_id: stateObj.entity_id,
|
||||||
label_mode: sourceObj?.label_mode ?? allSource?.label_mode,
|
label_mode: sourceObj?.label_mode ?? allSource?.label_mode,
|
||||||
attribute: sourceObj?.attribute ?? allSource?.attribute,
|
attribute: sourceObj?.attribute ?? allSource?.attribute,
|
||||||
|
unit: sourceObj?.unit ?? allSource?.unit,
|
||||||
focus: sourceObj
|
focus: sourceObj
|
||||||
? (sourceObj.focus ?? true)
|
? (sourceObj.focus ?? true)
|
||||||
: (allSource?.focus ?? true),
|
: (allSource?.focus ?? true),
|
||||||
@@ -446,6 +449,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
color: this._getColor(entityConf.entity),
|
color: this._getColor(entityConf.entity),
|
||||||
label_mode: entityConf.label_mode,
|
label_mode: entityConf.label_mode,
|
||||||
attribute: entityConf.attribute,
|
attribute: entityConf.attribute,
|
||||||
|
unit: entityConf.unit,
|
||||||
focus: entityConf.focus,
|
focus: entityConf.focus,
|
||||||
name: entityConf.name,
|
name: entityConf.name,
|
||||||
})),
|
})),
|
||||||
|
@@ -323,6 +323,7 @@ interface GeoLocationSourceConfig {
|
|||||||
source: string;
|
source: string;
|
||||||
label_mode?: "name" | "state" | "attribute" | "icon";
|
label_mode?: "name" | "state" | "attribute" | "icon";
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
|
unit?: string;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -40,6 +40,7 @@ export const mapEntitiesConfigStruct = union([
|
|||||||
entity: string(),
|
entity: string(),
|
||||||
label_mode: optional(string()),
|
label_mode: optional(string()),
|
||||||
attribute: optional(string()),
|
attribute: optional(string()),
|
||||||
|
unit: optional(string()),
|
||||||
focus: optional(boolean()),
|
focus: optional(boolean()),
|
||||||
name: optional(string()),
|
name: optional(string()),
|
||||||
}),
|
}),
|
||||||
@@ -51,6 +52,7 @@ const geoSourcesConfigStruct = union([
|
|||||||
source: string(),
|
source: string(),
|
||||||
label_mode: optional(string()),
|
label_mode: optional(string()),
|
||||||
attribute: optional(string()),
|
attribute: optional(string()),
|
||||||
|
unit: optional(string()),
|
||||||
focus: optional(boolean()),
|
focus: optional(boolean()),
|
||||||
}),
|
}),
|
||||||
string(),
|
string(),
|
||||||
|
@@ -139,7 +139,7 @@ export class HuiDialogSelectDashboard extends LitElement {
|
|||||||
...(this._params!.dashboards || (await fetchDashboards(this.hass))),
|
...(this._params!.dashboards || (await fetchDashboards(this.hass))),
|
||||||
];
|
];
|
||||||
|
|
||||||
const currentPath = this._fromUrlPath || this.hass.defaultPanel;
|
const currentPath = this._fromUrlPath || this.hass.sidebar.defaultPanel;
|
||||||
for (const dashboard of this._dashboards!) {
|
for (const dashboard of this._dashboards!) {
|
||||||
if (dashboard.url_path !== currentPath) {
|
if (dashboard.url_path !== currentPath) {
|
||||||
this._toUrlPath = dashboard.url_path;
|
this._toUrlPath = dashboard.url_path;
|
||||||
|
@@ -77,7 +77,7 @@ export class HuiDialogSelectView extends LitElement {
|
|||||||
"ui.panel.lovelace.editor.select_view.dashboard_label"
|
"ui.panel.lovelace.editor.select_view.dashboard_label"
|
||||||
)}
|
)}
|
||||||
.disabled=${!this._dashboards.length}
|
.disabled=${!this._dashboards.length}
|
||||||
.value=${this._urlPath || this.hass.defaultPanel}
|
.value=${this._urlPath || this.hass.sidebar.defaultPanel}
|
||||||
@selected=${this._dashboardChanged}
|
@selected=${this._dashboardChanged}
|
||||||
@closed=${stopPropagation}
|
@closed=${stopPropagation}
|
||||||
fixedMenuPosition
|
fixedMenuPosition
|
||||||
|
@@ -1109,6 +1109,16 @@ class HUIRoot extends LitElement {
|
|||||||
sl-tab[aria-selected="true"] .edit-icon {
|
sl-tab[aria-selected="true"] .edit-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
sl-tab::part(base) {
|
||||||
|
padding-inline-start: var(
|
||||||
|
--ha-tab-padding-start,
|
||||||
|
var(--sl-spacing-large)
|
||||||
|
);
|
||||||
|
padding-inline-end: var(
|
||||||
|
--ha-tab-padding-end,
|
||||||
|
var(--sl-spacing-large)
|
||||||
|
);
|
||||||
|
}
|
||||||
sl-tab::part(base) {
|
sl-tab::part(base) {
|
||||||
padding-top: calc((var(--header-height) - 20px) / 2);
|
padding-top: calc((var(--header-height) - 20px) / 2);
|
||||||
padding-bottom: calc((var(--header-height) - 20px) / 2 - 2px);
|
padding-bottom: calc((var(--header-height) - 20px) / 2 - 2px);
|
||||||
|
@@ -6,8 +6,8 @@ import "../../components/ha-select";
|
|||||||
import "../../components/ha-settings-row";
|
import "../../components/ha-settings-row";
|
||||||
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
|
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
|
||||||
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
||||||
import { setDefaultPanel } from "../../data/panel";
|
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { saveFrontendUserData } from "../../data/frontend";
|
||||||
|
|
||||||
@customElement("ha-pick-dashboard-row")
|
@customElement("ha-pick-dashboard-row")
|
||||||
class HaPickDashboardRow extends LitElement {
|
class HaPickDashboardRow extends LitElement {
|
||||||
@@ -37,7 +37,7 @@ class HaPickDashboardRow extends LitElement {
|
|||||||
"ui.panel.profile.dashboard.dropdown_label"
|
"ui.panel.profile.dashboard.dropdown_label"
|
||||||
)}
|
)}
|
||||||
.disabled=${!this._dashboards?.length}
|
.disabled=${!this._dashboards?.length}
|
||||||
.value=${this.hass.defaultPanel}
|
.value=${this.hass.sidebar.defaultPanel}
|
||||||
@selected=${this._dashboardChanged}
|
@selected=${this._dashboardChanged}
|
||||||
naturalMenuWidth
|
naturalMenuWidth
|
||||||
>
|
>
|
||||||
@@ -71,12 +71,16 @@ class HaPickDashboardRow extends LitElement {
|
|||||||
this._dashboards = await fetchDashboards(this.hass);
|
this._dashboards = await fetchDashboards(this.hass);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dashboardChanged(ev) {
|
private async _dashboardChanged(ev) {
|
||||||
const urlPath = ev.target.value;
|
const urlPath = ev.target.value;
|
||||||
if (!urlPath || urlPath === this.hass.defaultPanel) {
|
if (!urlPath || urlPath === this.hass.sidebar.defaultPanel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setDefaultPanel(this, urlPath);
|
await saveFrontendUserData(this.hass!.connection, "sidebar", {
|
||||||
|
panelOrder: this.hass!.sidebar.panelOrder,
|
||||||
|
hiddenPanels: this.hass!.sidebar.hiddenPanels,
|
||||||
|
defaultPanel: urlPath,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -167,6 +167,10 @@ class HaProfileSectionGeneral extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
</ha-settings-row>
|
</ha-settings-row>
|
||||||
|
<ha-pick-dashboard-row
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
.hass=${this.hass}
|
||||||
|
></ha-pick-dashboard-row>
|
||||||
${this.hass.user!.is_admin
|
${this.hass.user!.is_admin
|
||||||
? html`
|
? html`
|
||||||
<ha-advanced-mode-row
|
<ha-advanced-mode-row
|
||||||
@@ -200,10 +204,6 @@ class HaProfileSectionGeneral extends LitElement {
|
|||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></ha-pick-theme-row>
|
></ha-pick-theme-row>
|
||||||
<ha-pick-dashboard-row
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
.hass=${this.hass}
|
|
||||||
></ha-pick-dashboard-row>
|
|
||||||
${this.hass.dockedSidebar !== "auto" || !this.narrow
|
${this.hass.dockedSidebar !== "auto" || !this.narrow
|
||||||
? html`
|
? html`
|
||||||
<ha-force-narrow-row
|
<ha-force-narrow-row
|
||||||
|
@@ -59,7 +59,11 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||||||
services: null as any,
|
services: null as any,
|
||||||
user: null as any,
|
user: null as any,
|
||||||
panelUrl: (this as any)._panelUrl,
|
panelUrl: (this as any)._panelUrl,
|
||||||
|
sidebar: {
|
||||||
defaultPanel: DEFAULT_PANEL,
|
defaultPanel: DEFAULT_PANEL,
|
||||||
|
hiddenPanels: [],
|
||||||
|
panelOrder: [],
|
||||||
|
},
|
||||||
language,
|
language,
|
||||||
selectedLanguage: null,
|
selectedLanguage: null,
|
||||||
locale: {
|
locale: {
|
||||||
|
@@ -7,20 +7,16 @@ interface DockSidebarParams {
|
|||||||
dock: HomeAssistant["dockedSidebar"];
|
dock: HomeAssistant["dockedSidebar"];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DefaultPanelParams {
|
|
||||||
defaultPanel: HomeAssistant["defaultPanel"];
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"hass-dock-sidebar": DockSidebarParams;
|
"hass-dock-sidebar": DockSidebarParams;
|
||||||
"hass-default-panel": DefaultPanelParams;
|
"hass-set-sidebar-data": HomeAssistant["sidebar"];
|
||||||
}
|
}
|
||||||
// for add event listener
|
// for add event listener
|
||||||
interface HTMLElementEventMap {
|
interface HTMLElementEventMap {
|
||||||
"hass-dock-sidebar": HASSDomEvent<DockSidebarParams>;
|
"hass-dock-sidebar": HASSDomEvent<DockSidebarParams>;
|
||||||
"hass-default-panel": HASSDomEvent<DefaultPanelParams>;
|
"hass-set-sidebar-data": HASSDomEvent<HomeAssistant["sidebar"]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +28,10 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
|||||||
this._updateHass({ dockedSidebar: ev.detail.dock });
|
this._updateHass({ dockedSidebar: ev.detail.dock });
|
||||||
storeState(this.hass!);
|
storeState(this.hass!);
|
||||||
});
|
});
|
||||||
this.addEventListener("hass-default-panel", (ev) => {
|
this.addEventListener("hass-set-sidebar-data", async (ev) => {
|
||||||
this._updateHass({ defaultPanel: ev.detail.defaultPanel });
|
this._updateHass({
|
||||||
|
sidebar: ev.detail,
|
||||||
|
});
|
||||||
storeState(this.hass!);
|
storeState(this.hass!);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -873,7 +873,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"service-picker": {
|
"service-picker": {
|
||||||
"action": "Action"
|
"action": "Action",
|
||||||
|
"no_match": "No matching actions found"
|
||||||
},
|
},
|
||||||
"service-control": {
|
"service-control": {
|
||||||
"required": "This field is required",
|
"required": "This field is required",
|
||||||
@@ -901,6 +902,8 @@
|
|||||||
"hidden": "{number} hidden",
|
"hidden": "{number} hidden",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"ungrouped": "Ungrouped",
|
"ungrouped": "Ungrouped",
|
||||||
|
"collapse": "Collapse",
|
||||||
|
"expand": "Expand",
|
||||||
"settings": {
|
"settings": {
|
||||||
"header": "Customize",
|
"header": "Customize",
|
||||||
"hide": "Hide column {title}",
|
"hide": "Hide column {title}",
|
||||||
@@ -1578,7 +1581,7 @@
|
|||||||
"upload_in_progress": "A backup upload is currently in progress. The action will automatically proceed once the upload process is complete.",
|
"upload_in_progress": "A backup upload is currently in progress. The action will automatically proceed once the upload process is complete.",
|
||||||
"restore_in_progress": "A backup restore is currently in progress. The action will automatically proceed once the restore process is complete.",
|
"restore_in_progress": "A backup restore is currently in progress. The action will automatically proceed once the restore process is complete.",
|
||||||
"wait_for_backup": "Wait for the backup creation to finish",
|
"wait_for_backup": "Wait for the backup creation to finish",
|
||||||
"error_backup_state": "An error occured while getting the current backup state. Error: {error}",
|
"error_backup_state": "An error occurred while getting the current backup state. Error: {error}",
|
||||||
"wait_for_upload": "Wait for backup upload to finish",
|
"wait_for_upload": "Wait for backup upload to finish",
|
||||||
"wait_for_restore": "Wait for backup restore to finish",
|
"wait_for_restore": "Wait for backup restore to finish",
|
||||||
"reload": {
|
"reload": {
|
||||||
@@ -1781,7 +1784,7 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"add": "Add devices via this device",
|
"add": "Add devices via this device",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"manage": "Manage zigbee device",
|
"manage": "Manage Zigbee device",
|
||||||
"reconfigure": "Reconfigure",
|
"reconfigure": "Reconfigure",
|
||||||
"view_network": "View network"
|
"view_network": "View network"
|
||||||
},
|
},
|
||||||
@@ -2377,7 +2380,7 @@
|
|||||||
},
|
},
|
||||||
"generate": {
|
"generate": {
|
||||||
"sync": {
|
"sync": {
|
||||||
"title": "Synchonization",
|
"title": "Synchronization",
|
||||||
"name": "Backup name",
|
"name": "Backup name",
|
||||||
"locations": "Locations",
|
"locations": "Locations",
|
||||||
"locations_description": "What locations you want to automatically backup to.",
|
"locations_description": "What locations you want to automatically backup to.",
|
||||||
@@ -5131,7 +5134,7 @@
|
|||||||
"restore_entity_id_selected": {
|
"restore_entity_id_selected": {
|
||||||
"button": "Recreate entity IDs of selected",
|
"button": "Recreate entity IDs of selected",
|
||||||
"confirm_title": "Recreate entity IDs?",
|
"confirm_title": "Recreate entity IDs?",
|
||||||
"confirm_text": "Are you sure you want to change the entity IDs of these entities? You will have to change you dashboards, automations and scripts to use the new entity IDs.",
|
"confirm_text": "Are you sure you want to change the entity IDs of these entities? You will have to change your dashboards, automations and scripts to use the new entity IDs.",
|
||||||
"changes": "The following entity IDs will be updated:"
|
"changes": "The following entity IDs will be updated:"
|
||||||
},
|
},
|
||||||
"delete_selected": {
|
"delete_selected": {
|
||||||
|
@@ -243,7 +243,11 @@ export interface HomeAssistant {
|
|||||||
vibrate: boolean;
|
vibrate: boolean;
|
||||||
debugConnection: boolean;
|
debugConnection: boolean;
|
||||||
dockedSidebar: "docked" | "always_hidden" | "auto";
|
dockedSidebar: "docked" | "always_hidden" | "auto";
|
||||||
|
sidebar: {
|
||||||
defaultPanel: string;
|
defaultPanel: string;
|
||||||
|
panelOrder: string[];
|
||||||
|
hiddenPanels: string[];
|
||||||
|
};
|
||||||
moreInfoEntityId: string | null;
|
moreInfoEntityId: string | null;
|
||||||
user?: CurrentUser;
|
user?: CurrentUser;
|
||||||
userData?: CoreFrontendUserData | null;
|
userData?: CoreFrontendUserData | null;
|
||||||
|
@@ -8,7 +8,7 @@ const STORED_STATE = [
|
|||||||
"debugConnection",
|
"debugConnection",
|
||||||
"suspendWhenHidden",
|
"suspendWhenHidden",
|
||||||
"enableShortcuts",
|
"enableShortcuts",
|
||||||
"defaultPanel",
|
"sidebar",
|
||||||
];
|
];
|
||||||
|
|
||||||
export function storeState(hass: HomeAssistant) {
|
export function storeState(hass: HomeAssistant) {
|
||||||
|
Reference in New Issue
Block a user