mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-14 11:49:26 +00:00
Compare commits
1 Commits
20240703.0
...
dashboard_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ebe5207b6e |
@@ -8,7 +8,6 @@
|
||||
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
|
||||
"postStartCommand": "script/bootstrap",
|
||||
"containerEnv": {
|
||||
"DEV_CONTAINER": "1",
|
||||
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
|
||||
},
|
||||
"customizations": {
|
||||
|
@@ -32,7 +32,4 @@ module.exports = {
|
||||
}
|
||||
return version[1];
|
||||
},
|
||||
isDevContainer() {
|
||||
return process.env.DEV_CONTAINER === "1";
|
||||
},
|
||||
};
|
||||
|
@@ -244,11 +244,11 @@ const createTranslations = async () => {
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const masterStream = gulp
|
||||
gulp
|
||||
.src(`${workDir}/en.json`)
|
||||
.pipe(new PassThrough({ objectMode: true }));
|
||||
masterStream.pipe(hashStream, { end: false });
|
||||
const mergesFinished = [finished(masterStream)];
|
||||
.pipe(new PassThrough({ objectMode: true }))
|
||||
.pipe(hashStream, { end: false });
|
||||
const mergesFinished = [];
|
||||
for (const translationFile of translationFiles) {
|
||||
const locale = basename(translationFile, ".json");
|
||||
const subtags = locale.split("-");
|
||||
|
@@ -40,12 +40,8 @@ const runDevServer = async ({
|
||||
compiler,
|
||||
contentBase,
|
||||
port,
|
||||
listenHost = undefined,
|
||||
listenHost = "localhost",
|
||||
}) => {
|
||||
if (listenHost === undefined) {
|
||||
// For dev container, we need to listen on all hosts
|
||||
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost";
|
||||
}
|
||||
const server = new WebpackDevServer(
|
||||
{
|
||||
hot: false,
|
||||
|
@@ -74,9 +74,6 @@ const createWebpackConfig = ({
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
parser: {
|
||||
worker: ["*context.audioWorklet.addModule()", "..."],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
@@ -95,15 +92,11 @@ const createWebpackConfig = ({
|
||||
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
splitChunks: {
|
||||
// Disable splitting for web workers and worklets because imports of
|
||||
// external chunks are broken for:
|
||||
// - ESM output: https://github.com/webpack/webpack/issues/17014
|
||||
// - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543
|
||||
chunks: (chunk) =>
|
||||
!chunk.canBeInitial() &&
|
||||
!new RegExp(`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`).test(
|
||||
chunk.name
|
||||
),
|
||||
// Disable splitting for web workers with ESM output
|
||||
// Imports of external chunks are broken
|
||||
chunks: latestBuild
|
||||
? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
|
@@ -232,5 +232,17 @@ http:
|
||||
</p>
|
||||
</div>
|
||||
</hc-layout>
|
||||
|
||||
<script>
|
||||
var _gaq = [["_setAccount", "UA-57927901-9"], ["_trackPageview"]];
|
||||
(function (d, t) {
|
||||
var g = d.createElement(t),
|
||||
s = d.getElementsByTagName(t)[0];
|
||||
g.src =
|
||||
("https:" == location.protocol ? "//ssl" : "//www") +
|
||||
".google-analytics.com/ga.js";
|
||||
s.parentNode.insertBefore(g, s);
|
||||
})(document, "script");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -14,6 +14,12 @@
|
||||
--background-color: #41bdf5;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
|
||||
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
||||
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
|
||||
s.parentNode.insertBefore(g,s)}(document,'script'));
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
|
||||
|
@@ -11,4 +11,10 @@
|
||||
font-size: initial;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
|
||||
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
||||
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
|
||||
s.parentNode.insertBefore(g,s)}(document,'script'));
|
||||
</script>
|
||||
</html>
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "../../../src/resources/safari-14-attachshadow-patch";
|
||||
import "./layout/hc-connect";
|
||||
|
||||
import("../../../src/resources/ha-style");
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { isFrontpageEmbed } from "../../util/is_frontpage";
|
||||
import { DemoConfig } from "../types";
|
||||
|
||||
export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
|
||||
@@ -6,20 +5,36 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
|
||||
views: [
|
||||
{
|
||||
type: "sections",
|
||||
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
|
||||
title: "Demo",
|
||||
path: "home",
|
||||
icon: "mdi:home-assistant",
|
||||
sections: [
|
||||
...(isFrontpageEmbed
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: "Welcome 👋",
|
||||
cards: [{ type: "custom:ha-demo-card" }],
|
||||
},
|
||||
]),
|
||||
{
|
||||
title: "Welcome 👋",
|
||||
cards: [{ type: "custom:ha-demo-card" }],
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.living_room_garden_shutter",
|
||||
name: "Garden",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.living_room_graveyard_shutter",
|
||||
name: "Rear",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.living_room_left_shutter",
|
||||
name: "Left",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.living_room_right_shutter",
|
||||
name: "Right",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "light.floor_lamp",
|
||||
@@ -45,11 +60,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
|
||||
detail: 1,
|
||||
name: "Temperature",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "cover.living_room_garden_shutter",
|
||||
name: "Blinds",
|
||||
},
|
||||
{
|
||||
type: "tile",
|
||||
entity: "media_player.living_room_nest_mini",
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import "./util/is_frontpage";
|
||||
import "../../src/resources/safari-14-attachshadow-patch";
|
||||
import "./ha-demo";
|
||||
|
||||
import("../../src/resources/ha-style");
|
||||
|
@@ -93,5 +93,16 @@
|
||||
}
|
||||
</script>
|
||||
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
|
||||
<script>
|
||||
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
|
||||
(function (d, t) {
|
||||
var g = d.createElement(t),
|
||||
s = d.getElementsByTagName(t)[0];
|
||||
g.src =
|
||||
("https:" == location.protocol ? "//ssl" : "//www") +
|
||||
".google-analytics.com/ga.js";
|
||||
s.parentNode.insertBefore(g, s);
|
||||
})(document, "script");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1 +0,0 @@
|
||||
export const isFrontpageEmbed = document.location.search === "?frontpage";
|
@@ -1,8 +1,6 @@
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { getStripDiacriticsFn } from "../../../src/util/fuse";
|
||||
|
||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: IFuseOptions<StoreAddon> = {
|
||||
@@ -10,8 +8,7 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
getFn: getStripDiacriticsFn,
|
||||
};
|
||||
const fuse = new Fuse(addons, options);
|
||||
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
// Compat needs to be first import
|
||||
import "../../src/resources/compatibility";
|
||||
import "../../src/resources/safari-14-attachshadow-patch";
|
||||
import "./hassio-main";
|
||||
|
||||
import("../../src/resources/ha-style");
|
||||
|
18
package.json
18
package.json
@@ -80,7 +80,7 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "1.5.1",
|
||||
"@material/web": "1.5.0",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
@@ -155,7 +155,7 @@
|
||||
"@babel/plugin-transform-runtime": "7.24.7",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.13.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.13.2",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.5.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
@@ -168,7 +168,7 @@
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.16",
|
||||
"@types/chromecast-caf-receiver": "6.0.15",
|
||||
"@types/chromecast-caf-sender": "1.0.10",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/glob": "8.1.0",
|
||||
@@ -178,15 +178,15 @@
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.7",
|
||||
"@types/mocha": "10.0.6",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/serve-handler": "6.1.4",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"@typescript-eslint/eslint-plugin": "7.13.1",
|
||||
"@typescript-eslint/parser": "7.13.1",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
@@ -200,7 +200,7 @@
|
||||
"eslint-import-resolver-webpack": "0.13.8",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-lit": "1.14.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.3",
|
||||
"eslint-plugin-lit-a11y": "4.1.2",
|
||||
"eslint-plugin-unused-imports": "4.0.0",
|
||||
"eslint-plugin-wc": "2.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
@@ -220,7 +220,7 @@
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.10",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.5.0",
|
||||
"mocha": "10.4.0",
|
||||
"object-hash": "3.0.0",
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
@@ -237,7 +237,7 @@
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.5.2",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.92.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
|
BIN
public/static/images/logo_twitter.png
Normal file
BIN
public/static/images/logo_twitter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="1200" height="1227" viewBox="0 0 1200 1227" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" fill="white"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 430 B |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240703.0"
|
||||
version = "20240610.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { stripDiacritics } from "../strip-diacritics";
|
||||
import { fuzzyScore } from "./filter";
|
||||
|
||||
/**
|
||||
@@ -20,10 +19,10 @@ export const fuzzySequentialMatch = (
|
||||
for (const word of item.strings) {
|
||||
const scores = fuzzyScore(
|
||||
filter,
|
||||
stripDiacritics(filter.toLowerCase()),
|
||||
filter.toLowerCase(),
|
||||
0,
|
||||
word,
|
||||
stripDiacritics(word.toLowerCase()),
|
||||
word.toLowerCase(),
|
||||
0,
|
||||
true
|
||||
);
|
||||
|
@@ -1,2 +0,0 @@
|
||||
export const stripDiacritics = (str: string) =>
|
||||
str.normalize("NFD").replace(/[\u0300-\u036F]/g, "");
|
@@ -1,313 +0,0 @@
|
||||
import "@material/mwc-list";
|
||||
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { createCloseHeading } from "../ha-dialog";
|
||||
import "../ha-list-item";
|
||||
import "../ha-sortable";
|
||||
import "../ha-button";
|
||||
import { DataTableColumnContainer, DataTableColumnData } from "./ha-data-table";
|
||||
import { DataTableSettingsDialogParams } from "./show-dialog-data-table-settings";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("dialog-data-table-settings")
|
||||
export class DialogDataTableSettings extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DataTableSettingsDialogParams;
|
||||
|
||||
@state() private _columnOrder?: string[];
|
||||
|
||||
@state() private _hiddenColumns?: string[];
|
||||
|
||||
public showDialog(params: DataTableSettingsDialogParams) {
|
||||
this._params = params;
|
||||
this._columnOrder = params.columnOrder;
|
||||
this._hiddenColumns = params.hiddenColumns;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
private _sortedColumns = memoizeOne(
|
||||
(
|
||||
columns: DataTableColumnContainer,
|
||||
columnOrder: string[] | undefined,
|
||||
hiddenColumns: string[] | undefined
|
||||
) =>
|
||||
Object.keys(columns)
|
||||
.filter((col) => !columns[col].hidden)
|
||||
.sort((a, b) => {
|
||||
const orderA = columnOrder?.indexOf(a) ?? -1;
|
||||
const orderB = columnOrder?.indexOf(b) ?? -1;
|
||||
const hiddenA =
|
||||
hiddenColumns?.includes(a) ?? Boolean(columns[a].defaultHidden);
|
||||
const hiddenB =
|
||||
hiddenColumns?.includes(b) ?? Boolean(columns[b].defaultHidden);
|
||||
if (hiddenA !== hiddenB) {
|
||||
return hiddenA ? 1 : -1;
|
||||
}
|
||||
if (orderA !== orderB) {
|
||||
if (orderA === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (orderB === -1) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return orderA - orderB;
|
||||
})
|
||||
.reduce(
|
||||
(arr, key) => {
|
||||
arr.push({ key, ...columns[key] });
|
||||
return arr;
|
||||
},
|
||||
[] as (DataTableColumnData & { key: string })[]
|
||||
)
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const columns = this._sortedColumns(
|
||||
this._params.columns,
|
||||
this._columnOrder,
|
||||
this._hiddenColumns
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.components.data-table.settings.header")
|
||||
)}
|
||||
>
|
||||
<ha-sortable
|
||||
@item-moved=${this._columnMoved}
|
||||
draggable-selector=".draggable"
|
||||
handle-selector=".handle"
|
||||
>
|
||||
<mwc-list>
|
||||
${repeat(
|
||||
columns,
|
||||
(col) => col.key,
|
||||
(col, _idx) => {
|
||||
const canMove = !col.main && col.moveable !== false;
|
||||
const canHide = !col.main && col.hideable !== false;
|
||||
const isVisible = !(this._columnOrder &&
|
||||
this._columnOrder.includes(col.key)
|
||||
? this._hiddenColumns?.includes(col.key) ?? col.defaultHidden
|
||||
: col.defaultHidden);
|
||||
|
||||
return html`<ha-list-item
|
||||
hasMeta
|
||||
class=${classMap({
|
||||
hidden: !isVisible,
|
||||
draggable: canMove && isVisible,
|
||||
})}
|
||||
graphic="icon"
|
||||
noninteractive
|
||||
>${col.title || col.label || col.key}
|
||||
${canMove && isVisible
|
||||
? html`<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDrag}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
tabindex="0"
|
||||
class="action"
|
||||
.disabled=${!canHide}
|
||||
.hidden=${!isVisible}
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="meta"
|
||||
.label=${this.hass!.localize(
|
||||
`ui.components.data-table.settings.${isVisible ? "hide" : "show"}`,
|
||||
{ title: typeof col.title === "string" ? col.title : "" }
|
||||
)}
|
||||
.column=${col.key}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
</ha-list-item>`;
|
||||
}
|
||||
)}
|
||||
</mwc-list>
|
||||
</ha-sortable>
|
||||
<ha-button slot="secondaryAction" @click=${this._reset}
|
||||
>${this.hass.localize(
|
||||
"ui.components.data-table.settings.restore"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.components.data-table.settings.done")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _columnMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
const columns = this._sortedColumns(
|
||||
this._params.columns,
|
||||
this._columnOrder,
|
||||
this._hiddenColumns
|
||||
);
|
||||
|
||||
const columnOrder = columns.map((column) => column.key);
|
||||
|
||||
const option = columnOrder.splice(oldIndex, 1)[0];
|
||||
columnOrder.splice(newIndex, 0, option);
|
||||
|
||||
this._columnOrder = columnOrder;
|
||||
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
_toggle(ev) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const column = ev.target.column;
|
||||
const wasHidden = ev.target.hidden;
|
||||
|
||||
const hidden = [
|
||||
...(this._hiddenColumns ??
|
||||
Object.entries(this._params.columns)
|
||||
.filter(([_key, col]) => col.defaultHidden)
|
||||
.map(([key]) => key)),
|
||||
];
|
||||
if (wasHidden && hidden.includes(column)) {
|
||||
hidden.splice(hidden.indexOf(column), 1);
|
||||
} else if (!wasHidden) {
|
||||
hidden.push(column);
|
||||
}
|
||||
|
||||
const columns = this._sortedColumns(
|
||||
this._params.columns,
|
||||
this._columnOrder,
|
||||
hidden
|
||||
);
|
||||
|
||||
if (!this._columnOrder) {
|
||||
this._columnOrder = columns.map((col) => col.key);
|
||||
} else {
|
||||
const newOrder = this._columnOrder.filter((col) => col !== column);
|
||||
|
||||
// Array.findLastIndex when supported or core-js polyfill
|
||||
const findLastIndex = (
|
||||
arr: Array<any>,
|
||||
fn: (item: any, index: number, arr: Array<any>) => boolean
|
||||
) => {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
if (fn(arr[i], i, arr)) return i;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
let lastMoveable = findLastIndex(
|
||||
newOrder,
|
||||
(col) =>
|
||||
col !== column &&
|
||||
!hidden.includes(col) &&
|
||||
!this._params!.columns[col].main &&
|
||||
this._params!.columns[col].moveable !== false
|
||||
);
|
||||
|
||||
if (lastMoveable === -1) {
|
||||
lastMoveable = newOrder.length - 1;
|
||||
}
|
||||
|
||||
columns.forEach((col) => {
|
||||
if (!newOrder.includes(col.key)) {
|
||||
if (col.moveable === false) {
|
||||
newOrder.unshift(col.key);
|
||||
} else {
|
||||
newOrder.splice(lastMoveable + 1, 0, col.key);
|
||||
}
|
||||
|
||||
if (col.defaultHidden) {
|
||||
hidden.push(col.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._columnOrder = newOrder;
|
||||
}
|
||||
|
||||
this._hiddenColumns = hidden;
|
||||
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._columnOrder = undefined;
|
||||
this._hiddenColumns = undefined;
|
||||
|
||||
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-max-width: 500px;
|
||||
--dialog-z-index: 10;
|
||||
--dialog-content-padding: 0 8px;
|
||||
}
|
||||
@media all and (max-width: 451px) {
|
||||
ha-dialog {
|
||||
--vertical-align-dialog: flex-start;
|
||||
--dialog-surface-margin-top: 250px;
|
||||
--ha-dialog-border-radius: 28px 28px 0 0;
|
||||
--mdc-dialog-min-height: calc(100% - 250px);
|
||||
--mdc-dialog-max-height: calc(100% - 250px);
|
||||
}
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding: 12px;
|
||||
overflow: visible;
|
||||
}
|
||||
.hidden {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
.handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
ha-icon-button {
|
||||
display: block;
|
||||
margin: -12px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-data-table-settings": DialogDataTableSettings;
|
||||
}
|
||||
}
|
@@ -65,10 +65,6 @@ export interface DataTableSortColumnData {
|
||||
valueColumn?: string;
|
||||
direction?: SortingDirection;
|
||||
groupable?: boolean;
|
||||
moveable?: boolean;
|
||||
hideable?: boolean;
|
||||
defaultHidden?: boolean;
|
||||
showNarrow?: boolean;
|
||||
}
|
||||
|
||||
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
@@ -83,7 +79,6 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
| "overflow-menu"
|
||||
| "flex";
|
||||
template?: (row: T) => TemplateResult | string | typeof nothing;
|
||||
extraTemplate?: (row: T) => TemplateResult | string | typeof nothing;
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
grows?: boolean;
|
||||
@@ -110,8 +105,6 @@ const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||
export class HaDataTable extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Object }) public columns: DataTableColumnContainer = {};
|
||||
|
||||
@property({ type: Array }) public data: DataTableRowData[] = [];
|
||||
@@ -152,10 +145,6 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups?: string[];
|
||||
|
||||
@property({ attribute: false }) public hiddenColumns?: string[];
|
||||
|
||||
@property({ attribute: false }) public columnOrder?: string[];
|
||||
|
||||
@state() private _filterable = false;
|
||||
|
||||
@state() private _filter = "";
|
||||
@@ -246,7 +235,6 @@ export class HaDataTable extends LitElement {
|
||||
(column: ClonedDataTableColumnData) => {
|
||||
delete column.title;
|
||||
delete column.template;
|
||||
delete column.extraTemplate;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -284,44 +272,12 @@ export class HaDataTable extends LitElement {
|
||||
this._sortFilterData();
|
||||
}
|
||||
|
||||
if (properties.has("selectable") || properties.has("hiddenColumns")) {
|
||||
if (properties.has("selectable")) {
|
||||
this._items = [...this._items];
|
||||
}
|
||||
}
|
||||
|
||||
private _sortedColumns = memoizeOne(
|
||||
(columns: DataTableColumnContainer, columnOrder?: string[]) => {
|
||||
if (!columnOrder || !columnOrder.length) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
return Object.keys(columns)
|
||||
.sort((a, b) => {
|
||||
const orderA = columnOrder!.indexOf(a);
|
||||
const orderB = columnOrder!.indexOf(b);
|
||||
if (orderA !== orderB) {
|
||||
if (orderA === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (orderB === -1) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return orderA - orderB;
|
||||
})
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = columns[key];
|
||||
return obj;
|
||||
}, {}) as DataTableColumnContainer;
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const columns = this._sortedColumns(this.columns, this.columnOrder);
|
||||
|
||||
const renderRow = (row: DataTableRowData, index: number) =>
|
||||
this._renderRow(columns, this.narrow, row, index);
|
||||
|
||||
return html`
|
||||
<div class="mdc-data-table">
|
||||
<slot name="header" @slotchange=${this._calcTableHeight}>
|
||||
@@ -370,14 +326,9 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(columns).map(([key, column]) => {
|
||||
if (
|
||||
column.hidden ||
|
||||
(this.columnOrder && this.columnOrder.includes(key)
|
||||
? this.hiddenColumns?.includes(key) ?? column.defaultHidden
|
||||
: column.defaultHidden)
|
||||
) {
|
||||
return nothing;
|
||||
${Object.entries(this.columns).map(([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return "";
|
||||
}
|
||||
const sorted = key === this.sortColumn;
|
||||
const classes = {
|
||||
@@ -448,7 +399,7 @@ export class HaDataTable extends LitElement {
|
||||
@scroll=${this._saveScrollPos}
|
||||
.items=${this._items}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${renderRow}
|
||||
.renderItem=${this._renderRow}
|
||||
></lit-virtualizer>
|
||||
`}
|
||||
</div>
|
||||
@@ -458,12 +409,7 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row;
|
||||
|
||||
private _renderRow = (
|
||||
columns: DataTableColumnContainer,
|
||||
narrow: boolean,
|
||||
row: DataTableRowData,
|
||||
index: number
|
||||
) => {
|
||||
private _renderRow = (row: DataTableRowData, index: number) => {
|
||||
// not sure how this happens...
|
||||
if (!row) {
|
||||
return nothing;
|
||||
@@ -508,14 +454,8 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(columns).map(([key, column]) => {
|
||||
if (
|
||||
(narrow && !column.main && !column.showNarrow) ||
|
||||
column.hidden ||
|
||||
(this.columnOrder && this.columnOrder.includes(key)
|
||||
? this.hiddenColumns?.includes(key) ?? column.defaultHidden
|
||||
: column.defaultHidden)
|
||||
) {
|
||||
${Object.entries(this.columns).map(([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
@@ -542,38 +482,7 @@ export class HaDataTable extends LitElement {
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row)
|
||||
: narrow && column.main
|
||||
? html`<div class="primary">${row[key]}</div>
|
||||
<div class="secondary">
|
||||
${Object.entries(columns)
|
||||
.filter(
|
||||
([key2, column2]) =>
|
||||
!column2.hidden &&
|
||||
!column2.main &&
|
||||
!column2.showNarrow &&
|
||||
!(this.columnOrder &&
|
||||
this.columnOrder.includes(key2)
|
||||
? this.hiddenColumns?.includes(key2) ??
|
||||
column2.defaultHidden
|
||||
: column2.defaultHidden)
|
||||
)
|
||||
.map(
|
||||
([key2, column2], i) =>
|
||||
html`${i !== 0
|
||||
? " ⸱ "
|
||||
: nothing}${column2.template
|
||||
? column2.template(row)
|
||||
: row[key2]}`
|
||||
)}
|
||||
</div>
|
||||
${column.extraTemplate
|
||||
? column.extraTemplate(row)
|
||||
: nothing}`
|
||||
: html`${row[key]}${column.extraTemplate
|
||||
? column.extraTemplate(row)
|
||||
: nothing}`}
|
||||
${column.template ? column.template(row) : row[key]}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@@ -952,7 +861,6 @@ export class HaDataTable extends LitElement {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell {
|
||||
|
@@ -1,26 +0,0 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { DataTableColumnContainer } from "./ha-data-table";
|
||||
|
||||
export interface DataTableSettingsDialogParams {
|
||||
columns: DataTableColumnContainer;
|
||||
onUpdate: (
|
||||
columnOrder: string[] | undefined,
|
||||
hiddenColumns: string[] | undefined
|
||||
) => void;
|
||||
hiddenColumns?: string[];
|
||||
columnOrder?: string[];
|
||||
}
|
||||
|
||||
export const loadDataTableSettingsDialog = () =>
|
||||
import("./dialog-data-table-settings");
|
||||
|
||||
export const showDataTableSettingsDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: DataTableSettingsDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-data-table-settings",
|
||||
dialogImport: loadDataTableSettingsDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -1,6 +1,5 @@
|
||||
import { expose } from "comlink";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
DataTableRowData,
|
||||
@@ -13,18 +12,20 @@ const filterData = (
|
||||
columns: SortableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
filter = stripDiacritics(filter.toLowerCase());
|
||||
filter = filter.toUpperCase();
|
||||
return data.filter((row) =>
|
||||
Object.entries(columns).some((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
if (column.filterable) {
|
||||
const value = String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
);
|
||||
|
||||
if (stripDiacritics(value).toLowerCase().includes(filter)) {
|
||||
if (
|
||||
String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
)
|
||||
.toUpperCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -90,8 +90,7 @@ class HaAnsiToHtml extends LitElement {
|
||||
|
||||
private _parseTextToColoredPre(text) {
|
||||
const pre = document.createElement("pre");
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
|
||||
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
||||
let i = 0;
|
||||
|
||||
const state: State = {
|
||||
|
@@ -7,7 +7,6 @@ import { mdiRestore } from "@mdi/js";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { conditionalClamp } from "../common/number/clamp";
|
||||
|
||||
type GridSizeValue = {
|
||||
rows?: number;
|
||||
@@ -43,10 +42,6 @@ export class HaGridSizeEditor extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const disabledColumns =
|
||||
this.columnMin !== undefined && this.columnMin === this.columnMax;
|
||||
const disabledRows =
|
||||
this.rowMin !== undefined && this.rowMin === this.rowMax;
|
||||
return html`
|
||||
<div class="grid">
|
||||
<ha-grid-layout-slider
|
||||
@@ -60,7 +55,6 @@ export class HaGridSizeEditor extends LitElement {
|
||||
.value=${this.value?.columns}
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
.disabled=${disabledColumns}
|
||||
></ha-grid-layout-slider>
|
||||
<ha-grid-layout-slider
|
||||
aria-label=${this.hass.localize(
|
||||
@@ -74,7 +68,6 @@ export class HaGridSizeEditor extends LitElement {
|
||||
.value=${this.value?.rows}
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
.disabled=${disabledRows}
|
||||
></ha-grid-layout-slider>
|
||||
${!this.isDefault
|
||||
? html`
|
||||
@@ -107,11 +100,17 @@ export class HaGridSizeEditor extends LitElement {
|
||||
.map((_, index) => {
|
||||
const row = Math.floor(index / this.columns) + 1;
|
||||
const column = (index % this.columns) + 1;
|
||||
const disabled =
|
||||
(this.rowMin !== undefined && row < this.rowMin) ||
|
||||
(this.rowMax !== undefined && row > this.rowMax) ||
|
||||
(this.columnMin !== undefined && column < this.columnMin) ||
|
||||
(this.columnMax !== undefined && column > this.columnMax);
|
||||
return html`
|
||||
<div
|
||||
class="cell"
|
||||
data-row=${row}
|
||||
data-column=${column}
|
||||
?disabled=${disabled}
|
||||
@click=${this._cellClick}
|
||||
></div>
|
||||
`;
|
||||
@@ -127,16 +126,11 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
_cellClick(ev) {
|
||||
const cell = ev.currentTarget as HTMLElement;
|
||||
if (cell.getAttribute("disabled") !== null) return;
|
||||
const rows = Number(cell.getAttribute("data-row"));
|
||||
const columns = Number(cell.getAttribute("data-column"));
|
||||
const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax);
|
||||
const clampedColumn = conditionalClamp(
|
||||
columns,
|
||||
this.columnMin,
|
||||
this.columnMax
|
||||
);
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { rows: clampedRow, columns: clampedColumn },
|
||||
value: { rows, columns },
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,92 +1,72 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCamera } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type QrScanner from "qr-scanner";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { addExternalBarCodeListener } from "../external_app/external_app_entrypoint";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-alert";
|
||||
import "./ha-button-menu";
|
||||
import "./ha-list-item";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@customElement("ha-qr-scanner")
|
||||
class HaQrScanner extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public localize!: LocalizeFunc;
|
||||
|
||||
@property() public description?: string;
|
||||
|
||||
@property({ attribute: "alternative_option_label" })
|
||||
public alternativeOptionLabel?: string;
|
||||
|
||||
@property() public error?: string;
|
||||
|
||||
@state() private _cameras?: QrScanner.Camera[];
|
||||
|
||||
@state() private _manual = false;
|
||||
@state() private _error?: string;
|
||||
|
||||
private _qrScanner?: QrScanner;
|
||||
|
||||
private _qrNotFoundCount = 0;
|
||||
|
||||
private _removeListener?: UnsubscribeFunc;
|
||||
@query("video", true) private _video!: HTMLVideoElement;
|
||||
|
||||
@query("video", true) private _video?: HTMLVideoElement;
|
||||
|
||||
@query("#canvas-container", true) private _canvasContainer?: HTMLDivElement;
|
||||
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
|
||||
|
||||
@query("ha-textfield") private _manualInput?: HaTextField;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._qrNotFoundCount = 0;
|
||||
if (this._nativeBarcodeScanner) {
|
||||
this._closeExternalScanner();
|
||||
}
|
||||
if (this._qrScanner) {
|
||||
this._qrScanner.stop();
|
||||
this._qrScanner.destroy();
|
||||
this._qrScanner = undefined;
|
||||
}
|
||||
while (this._canvasContainer?.lastChild) {
|
||||
while (this._canvasContainer.lastChild) {
|
||||
this._canvasContainer.removeChild(this._canvasContainer.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
if (this.hasUpdated && navigator.mediaDevices) {
|
||||
this._loadQrScanner();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._loadQrScanner();
|
||||
if (navigator.mediaDevices) {
|
||||
this._loadQrScanner();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("error") && this.error) {
|
||||
alert(`error: ${this.error}`);
|
||||
this._notifyExternalScanner(this.error);
|
||||
if (changedProps.has("_error") && this._error) {
|
||||
fireEvent(this, "qr-code-error", { message: this._error });
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._nativeBarcodeScanner && !this._manual) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`${this.error
|
||||
? html`<ha-alert alert-type="error">${this.error}</ha-alert>`
|
||||
protected render(): TemplateResult {
|
||||
return html`${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${navigator.mediaDevices && !this._manual
|
||||
${navigator.mediaDevices
|
||||
? html`<video></video>
|
||||
<div id="canvas-container">
|
||||
${this._cameras && this._cameras.length > 1
|
||||
@@ -100,26 +80,21 @@ class HaQrScanner extends LitElement {
|
||||
></ha-icon-button>
|
||||
${this._cameras!.map(
|
||||
(camera) => html`
|
||||
<ha-list-item
|
||||
<mwc-list-item
|
||||
.value=${camera.id}
|
||||
@click=${this._cameraChanged}
|
||||
>${camera.label}</mwc-list-item
|
||||
>
|
||||
${camera.label}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
: ""}
|
||||
</div>`
|
||||
: html`${this._manual
|
||||
? nothing
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this.localize(
|
||||
"ui.components.qr-scanner.only_https_supported"
|
||||
)
|
||||
: this.localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>`}
|
||||
: html`<ha-alert alert-type="warning">
|
||||
${!window.isSecureContext
|
||||
? this.localize("ui.components.qr-scanner.only_https_supported")
|
||||
: this.localize("ui.components.qr-scanner.not_supported")}
|
||||
</ha-alert>
|
||||
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<ha-textfield
|
||||
@@ -127,44 +102,33 @@ class HaQrScanner extends LitElement {
|
||||
@keyup=${this._manualKeyup}
|
||||
@paste=${this._manualPaste}
|
||||
></ha-textfield>
|
||||
<mwc-button @click=${this._manualSubmit}>
|
||||
${this.localize("ui.common.submit")}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._manualSubmit}
|
||||
>${this.localize("ui.common.submit")}</mwc-button
|
||||
>
|
||||
</div>`}`;
|
||||
}
|
||||
|
||||
private get _nativeBarcodeScanner(): boolean {
|
||||
return Boolean(this.hass.auth.external?.config.hasBarCodeScanner);
|
||||
}
|
||||
|
||||
private async _loadQrScanner() {
|
||||
if (this._nativeBarcodeScanner) {
|
||||
this._openExternalScanner();
|
||||
return;
|
||||
}
|
||||
if (!navigator.mediaDevices) {
|
||||
return;
|
||||
}
|
||||
const QrScanner = (await import("qr-scanner")).default;
|
||||
if (!(await QrScanner.hasCamera())) {
|
||||
this._reportError("No camera found");
|
||||
this._error = "No camera found";
|
||||
return;
|
||||
}
|
||||
QrScanner.WORKER_PATH = "/static/js/qr-scanner-worker.min.js";
|
||||
this._listCameras(QrScanner);
|
||||
this._qrScanner = new QrScanner(
|
||||
this._video!,
|
||||
this._video,
|
||||
this._qrCodeScanned,
|
||||
this._qrCodeError
|
||||
);
|
||||
// @ts-ignore
|
||||
const canvas = this._qrScanner.$canvas;
|
||||
this._canvasContainer!.appendChild(canvas);
|
||||
this._canvasContainer.appendChild(canvas);
|
||||
canvas.style.display = "block";
|
||||
try {
|
||||
await this._qrScanner.start();
|
||||
} catch (err: any) {
|
||||
this._reportError(err);
|
||||
this._error = err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,16 +140,16 @@ class HaQrScanner extends LitElement {
|
||||
if (err === "No QR code found") {
|
||||
this._qrNotFoundCount++;
|
||||
if (this._qrNotFoundCount === 250) {
|
||||
this._reportError(err);
|
||||
this._error = err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this._reportError(err.message || err);
|
||||
this._error = err.message || err;
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err);
|
||||
};
|
||||
|
||||
private _qrCodeScanned = (qrCodeString: string): void => {
|
||||
private _qrCodeScanned = async (qrCodeString: string): Promise<void> => {
|
||||
this._qrNotFoundCount = 0;
|
||||
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
|
||||
};
|
||||
@@ -211,62 +175,6 @@ class HaQrScanner extends LitElement {
|
||||
this._qrScanner?.setCamera((ev.target as any).value);
|
||||
}
|
||||
|
||||
private _openExternalScanner() {
|
||||
this._removeListener = addExternalBarCodeListener((msg) => {
|
||||
if (msg.command === "bar_code/scan_result") {
|
||||
if (msg.payload.format !== "qr_code") {
|
||||
this._notifyExternalScanner(
|
||||
`Wrong barcode scanned! ${msg.payload.format}: ${msg.payload.rawValue}, we need a QR code.`
|
||||
);
|
||||
} else {
|
||||
this._qrCodeScanned(msg.payload.rawValue);
|
||||
}
|
||||
} else if (msg.command === "bar_code/aborted") {
|
||||
this._closeExternalScanner();
|
||||
if (msg.payload.reason === "canceled") {
|
||||
fireEvent(this, "qr-code-closed");
|
||||
} else {
|
||||
this._manual = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "bar_code/scan",
|
||||
payload: {
|
||||
title: this.title || "Scan QR code",
|
||||
description: this.description || "Scan a barcode.",
|
||||
alternative_option_label:
|
||||
this.alternativeOptionLabel || "Click to manually enter the barcode",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _closeExternalScanner() {
|
||||
this._removeListener?.();
|
||||
this._removeListener = undefined;
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "bar_code/close",
|
||||
});
|
||||
}
|
||||
|
||||
private _notifyExternalScanner(message: string) {
|
||||
if (!this.hass.auth.external) {
|
||||
return;
|
||||
}
|
||||
this.hass.auth.external.fireMessage({
|
||||
type: "bar_code/notify",
|
||||
payload: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
this.error = undefined;
|
||||
}
|
||||
|
||||
private _reportError(message: string) {
|
||||
fireEvent(this, "qr-code-error", { message });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
canvas {
|
||||
width: 100%;
|
||||
@@ -302,7 +210,6 @@ declare global {
|
||||
interface HASSDomEvents {
|
||||
"qr-code-scanned": { value: string };
|
||||
"qr-code-error": { message: string };
|
||||
"qr-code-closed": undefined;
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
|
@@ -11,7 +11,6 @@ import {
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { AreaSelector } from "../../data/selector";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
@@ -38,8 +37,6 @@ export class HaAreaSelector extends LitElement {
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
private _hasIntegration(selector: AreaSelector) {
|
||||
@@ -75,12 +72,6 @@ export class HaAreaSelector extends LitElement {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
if (!this._configEntries && this._hasIntegration(this.selector)) {
|
||||
this._configEntries = [];
|
||||
getConfigEntries(this.hass).then((entries) => {
|
||||
this._configEntries = entries;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -145,9 +136,7 @@ export class HaAreaSelector extends LitElement {
|
||||
const deviceIntegrations = this._entitySources
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
Object.values(this.hass.entities),
|
||||
Object.values(this.hass.devices),
|
||||
this._configEntries
|
||||
Object.values(this.hass.entities)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
@@ -11,7 +11,6 @@ import {
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { DeviceSelector } from "../../data/selector";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
@@ -28,8 +27,6 @@ export class HaDeviceSelector extends LitElement {
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -78,12 +75,6 @@ export class HaDeviceSelector extends LitElement {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
if (!this._configEntries && this._hasIntegration(this.selector)) {
|
||||
this._configEntries = [];
|
||||
getConfigEntries(this.hass).then((entries) => {
|
||||
this._configEntries = entries;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -132,9 +123,7 @@ export class HaDeviceSelector extends LitElement {
|
||||
const deviceIntegrations = this._entitySources
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
Object.values(this.hass.entities),
|
||||
Object.values(this.hass.devices),
|
||||
this._configEntries
|
||||
Object.values(this.hass.entities)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
@@ -11,7 +11,6 @@ import {
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { FloorSelector } from "../../data/selector";
|
||||
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
@@ -38,8 +37,6 @@ export class HaFloorSelector extends LitElement {
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
private _hasIntegration(selector: FloorSelector) {
|
||||
@@ -75,12 +72,6 @@ export class HaFloorSelector extends LitElement {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
if (!this._configEntries && this._hasIntegration(this.selector)) {
|
||||
this._configEntries = [];
|
||||
getConfigEntries(this.hass).then((entries) => {
|
||||
this._configEntries = entries;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -145,9 +136,7 @@ export class HaFloorSelector extends LitElement {
|
||||
const deviceIntegrations = this._entitySources
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
Object.values(this.hass.entities),
|
||||
Object.values(this.hass.devices),
|
||||
this._configEntries
|
||||
Object.values(this.hass.entities)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
|
@@ -65,8 +65,6 @@ interface ExtHassService extends Omit<HassService, "fields"> {
|
||||
Omit<HassService["fields"][string], "selector"> & {
|
||||
key: string;
|
||||
selector?: Selector;
|
||||
fields?: Record<string, Omit<HassService["fields"][string], "selector">>;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
>;
|
||||
hasSelector: string[];
|
||||
@@ -249,7 +247,20 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getTargetedEntities = memoizeOne((target, value) => {
|
||||
private _filterFields = memoizeOne(
|
||||
(serviceData: ExtHassService | undefined, value: this["value"]) =>
|
||||
serviceData?.fields?.filter(
|
||||
(field) =>
|
||||
!field.filter ||
|
||||
this._filterField(serviceData.target, field.filter, value)
|
||||
)
|
||||
);
|
||||
|
||||
private _filterField(
|
||||
target: ExtHassService["target"],
|
||||
filter: ExtHassService["fields"][number]["filter"],
|
||||
value: this["value"]
|
||||
) {
|
||||
const targetSelector = target ? { target } : { target: {} };
|
||||
const targetEntities =
|
||||
ensureArray(
|
||||
@@ -319,13 +330,6 @@ export class HaServiceControl extends LitElement {
|
||||
);
|
||||
});
|
||||
}
|
||||
return targetEntities;
|
||||
});
|
||||
|
||||
private _filterField(
|
||||
filter: ExtHassService["fields"][number]["filter"],
|
||||
targetEntities: string[]
|
||||
) {
|
||||
if (!targetEntities.length) {
|
||||
return false;
|
||||
}
|
||||
@@ -387,10 +391,7 @@ export class HaServiceControl extends LitElement {
|
||||
serviceData?.fields.some((field) => showOptionalToggle(field))
|
||||
);
|
||||
|
||||
const targetEntities = this._getTargetedEntities(
|
||||
serviceData?.target,
|
||||
this._value
|
||||
);
|
||||
const filteredFields = this._filterFields(serviceData, this._value);
|
||||
|
||||
const domain = this._value?.service
|
||||
? computeDomain(this._value.service)
|
||||
@@ -484,115 +485,75 @@ export class HaServiceControl extends LitElement {
|
||||
.defaultValue=${this._value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: serviceData?.fields.map((dataField) =>
|
||||
dataField.fields
|
||||
? html`<ha-expansion-panel
|
||||
leftChevron
|
||||
.expanded=${!dataField.collapsed}
|
||||
.header=${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
|
||||
) ||
|
||||
dataField.name ||
|
||||
dataField.key}
|
||||
>
|
||||
${Object.entries(dataField.fields).map(([key, field]) =>
|
||||
this._renderField(
|
||||
{ key, ...field },
|
||||
hasOptional,
|
||||
domain,
|
||||
serviceName,
|
||||
targetEntities
|
||||
)
|
||||
)}
|
||||
</ha-expansion-panel>`
|
||||
: this._renderField(
|
||||
dataField,
|
||||
hasOptional,
|
||||
domain,
|
||||
serviceName,
|
||||
targetEntities
|
||||
)
|
||||
)} `;
|
||||
: filteredFields?.map((dataField) => {
|
||||
const selector = dataField?.selector ?? { text: undefined };
|
||||
const type = Object.keys(selector)[0];
|
||||
const enhancedSelector = ["action", "condition", "trigger"].includes(
|
||||
type
|
||||
)
|
||||
? {
|
||||
[type]: {
|
||||
...selector[type],
|
||||
path: [dataField.key],
|
||||
},
|
||||
}
|
||||
: selector;
|
||||
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
|
||||
return dataField.selector &&
|
||||
(!dataField.advanced ||
|
||||
this.showAdvanced ||
|
||||
(this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined))
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${!showOptional
|
||||
? hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""
|
||||
: html`<ha-checkbox
|
||||
.key=${dataField.key}
|
||||
.checked=${this._checkedKeys.has(dataField.key) ||
|
||||
(this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined)}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
|
||||
) ||
|
||||
dataField.name ||
|
||||
dataField.key}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}</span
|
||||
>
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${enhancedSelector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this._value?.data
|
||||
? this._value.data[dataField.key]
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
@item-moved=${this._itemMoved}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: "";
|
||||
})} `;
|
||||
}
|
||||
|
||||
private _renderField = (
|
||||
dataField: ExtHassService["fields"][number],
|
||||
hasOptional: boolean,
|
||||
domain: string | undefined,
|
||||
serviceName: string | undefined,
|
||||
targetEntities: string[]
|
||||
) => {
|
||||
if (
|
||||
dataField.filter &&
|
||||
!this._filterField(dataField.filter, targetEntities)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const selector = dataField?.selector ?? { text: undefined };
|
||||
const type = Object.keys(selector)[0];
|
||||
const enhancedSelector = ["action", "condition", "trigger"].includes(type)
|
||||
? {
|
||||
[type]: {
|
||||
...selector[type],
|
||||
path: [dataField.key],
|
||||
},
|
||||
}
|
||||
: selector;
|
||||
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
|
||||
return dataField.selector &&
|
||||
(!dataField.advanced ||
|
||||
this.showAdvanced ||
|
||||
(this._value?.data && this._value.data[dataField.key] !== undefined))
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${!showOptional
|
||||
? hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""
|
||||
: html`<ha-checkbox
|
||||
.key=${dataField.key}
|
||||
.checked=${this._checkedKeys.has(dataField.key) ||
|
||||
(this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined)}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
|
||||
) ||
|
||||
dataField.name ||
|
||||
dataField.key}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}</span
|
||||
>
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${enhancedSelector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this._value?.data
|
||||
? this._value.data[dataField.key]
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
@item-moved=${this._itemMoved}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: "";
|
||||
};
|
||||
|
||||
private _localizeValueCallback = (key: string) => {
|
||||
if (!this._value?.service) {
|
||||
return "";
|
||||
@@ -878,11 +839,6 @@ export class HaServiceControl extends LitElement {
|
||||
.description p {
|
||||
direction: ltr;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: 0;
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -352,22 +352,6 @@ export const saveAutomationConfig = (
|
||||
config: AutomationConfig
|
||||
) => hass.callApi<void>("POST", `config/automation/config/${id}`, config);
|
||||
|
||||
export const normalizeAutomationConfig = <
|
||||
T extends Partial<AutomationConfig> | AutomationConfig,
|
||||
>(
|
||||
config: T
|
||||
): T => {
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
for (const key of ["trigger", "condition", "action"]) {
|
||||
const value = config[key];
|
||||
if (value && !Array.isArray(value)) {
|
||||
config[key] = [value];
|
||||
}
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
|
||||
initialAutomationEditorData = data;
|
||||
navigate("/config/automation/edit/new");
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { ManualAutomationConfig } from "./automation";
|
||||
import { ManualScriptConfig } from "./script";
|
||||
import { Selector } from "./selector";
|
||||
|
||||
export type BlueprintDomain = "automation" | "script";
|
||||
@@ -44,11 +42,6 @@ export interface BlueprintImportResult {
|
||||
validation_errors: string[] | null;
|
||||
}
|
||||
|
||||
export interface BlueprintSubstituteResults {
|
||||
automation: { substituted_config: ManualAutomationConfig };
|
||||
script: { substituted_config: ManualScriptConfig };
|
||||
}
|
||||
|
||||
export const fetchBlueprints = (hass: HomeAssistant, domain: BlueprintDomain) =>
|
||||
hass.callWS<Blueprints>({ type: "blueprint/list", domain });
|
||||
|
||||
@@ -98,18 +91,3 @@ export const getBlueprintSourceType = (
|
||||
}
|
||||
return "community";
|
||||
};
|
||||
|
||||
export const substituteBlueprint = <
|
||||
T extends BlueprintDomain = BlueprintDomain,
|
||||
>(
|
||||
hass: HomeAssistant,
|
||||
domain: T,
|
||||
path: string,
|
||||
input: Record<string, any>
|
||||
) =>
|
||||
hass.callWS<BlueprintSubstituteResults[T]>({
|
||||
type: "blueprint/substitute",
|
||||
domain,
|
||||
path,
|
||||
input,
|
||||
});
|
||||
|
@@ -5,7 +5,6 @@ import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import { ConfigEntry } from "./config_entries";
|
||||
import type { EntitySources } from "./entity_sources";
|
||||
|
||||
export {
|
||||
@@ -143,11 +142,9 @@ export const getDeviceEntityDisplayLookup = (
|
||||
|
||||
export const getDeviceIntegrationLookup = (
|
||||
entitySources: EntitySources,
|
||||
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[],
|
||||
devices?: DeviceRegistryEntry[],
|
||||
configEntries?: ConfigEntry[]
|
||||
): Record<string, Set<string>> => {
|
||||
const deviceIntegrations: Record<string, Set<string>> = {};
|
||||
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[]
|
||||
): Record<string, string[]> => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
for (const entity of entities) {
|
||||
const source = entitySources[entity.entity_id];
|
||||
@@ -155,22 +152,10 @@ export const getDeviceIntegrationLookup = (
|
||||
continue;
|
||||
}
|
||||
|
||||
deviceIntegrations[entity.device_id!] =
|
||||
deviceIntegrations[entity.device_id!] || new Set<string>();
|
||||
deviceIntegrations[entity.device_id!].add(source.domain);
|
||||
}
|
||||
// Lookup devices that have no entities
|
||||
if (devices && configEntries) {
|
||||
for (const device of devices) {
|
||||
for (const config_entry_id of device.config_entries) {
|
||||
const entry = configEntries.find((e) => e.entry_id === config_entry_id);
|
||||
if (entry?.domain) {
|
||||
deviceIntegrations[device.id] =
|
||||
deviceIntegrations[device.id] || new Set<string>();
|
||||
deviceIntegrations[device.id].add(entry.domain);
|
||||
}
|
||||
}
|
||||
if (!deviceIntegrations[entity.device_id!]) {
|
||||
deviceIntegrations[entity.device_id!] = [];
|
||||
}
|
||||
deviceIntegrations[entity.device_id!].push(source.domain);
|
||||
}
|
||||
return deviceIntegrations;
|
||||
};
|
||||
|
@@ -696,7 +696,7 @@ export const entityMeetsTargetSelector = (
|
||||
export const filterSelectorDevices = (
|
||||
filterDevice: DeviceSelectorFilter,
|
||||
device: DeviceRegistryEntry,
|
||||
deviceIntegrationLookup?: Record<string, Set<string>> | undefined
|
||||
deviceIntegrationLookup?: Record<string, string[]> | undefined
|
||||
): boolean => {
|
||||
const {
|
||||
manufacturer: filterManufacturer,
|
||||
@@ -713,7 +713,7 @@ export const filterSelectorDevices = (
|
||||
}
|
||||
|
||||
if (filterIntegration && deviceIntegrationLookup) {
|
||||
if (!deviceIntegrationLookup?.[device.id]?.has(filterIntegration)) {
|
||||
if (!deviceIntegrationLookup?.[device.id]?.includes(filterIntegration)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// Compat needs to be first import
|
||||
import "../resources/compatibility";
|
||||
import "../auth/ha-authorize";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
|
||||
import("../resources/ha-style");
|
||||
import("@polymer/polymer/lib/utils/settings").then(
|
||||
|
@@ -25,6 +25,7 @@ import { subscribePanels } from "../data/ws-panels";
|
||||
import { subscribeThemes } from "../data/ws-themes";
|
||||
import { subscribeUser } from "../data/ws-user";
|
||||
import type { ExternalAuth } from "../external_app/external_auth";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
|
||||
window.name = MAIN_WINDOW_NAME;
|
||||
(window as any).frontendVersion = __VERSION__;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
// Compat needs to be first import
|
||||
import "../resources/compatibility";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
|
||||
import { CSSResult } from "lit";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// Compat needs to be first import
|
||||
import "../resources/compatibility";
|
||||
import "../onboarding/ha-onboarding";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
|
||||
import("../resources/ha-style");
|
||||
import("@polymer/polymer/lib/utils/settings").then(
|
||||
|
@@ -10,18 +10,6 @@ const now = () => new Date().toISOString();
|
||||
const randomTime = () =>
|
||||
new Date(new Date().getTime() - Math.random() * 80 * 60 * 1000).toISOString();
|
||||
|
||||
const CAPABILITY_ATTRIBUTES = [
|
||||
"friendly_name",
|
||||
"unit_of_measurement",
|
||||
"icon",
|
||||
"entity_picture",
|
||||
"supported_features",
|
||||
"hidden",
|
||||
"assumed_state",
|
||||
"device_class",
|
||||
"state_class",
|
||||
"restored",
|
||||
];
|
||||
export class Entity {
|
||||
public domain: string;
|
||||
|
||||
@@ -41,28 +29,16 @@ export class Entity {
|
||||
|
||||
public hass?: any;
|
||||
|
||||
static CAPABILITY_ATTRIBUTES = new Set(CAPABILITY_ATTRIBUTES);
|
||||
|
||||
constructor(domain, objectId, state, attributes) {
|
||||
constructor(domain, objectId, state, baseAttributes) {
|
||||
this.domain = domain;
|
||||
this.objectId = objectId;
|
||||
this.entityId = `${domain}.${objectId}`;
|
||||
this.lastChanged = randomTime();
|
||||
this.lastUpdated = randomTime();
|
||||
this.state = String(state);
|
||||
|
||||
// These are the attributes that we always write to the state machine
|
||||
const baseAttributes = {};
|
||||
const capabilityAttributes =
|
||||
TYPES[domain]?.CAPABILITY_ATTRIBUTES || Entity.CAPABILITY_ATTRIBUTES;
|
||||
for (const key of Object.keys(attributes)) {
|
||||
if (capabilityAttributes.has(key)) {
|
||||
baseAttributes[key] = attributes[key];
|
||||
}
|
||||
}
|
||||
|
||||
this.baseAttributes = baseAttributes;
|
||||
this.attributes = attributes;
|
||||
this.attributes = baseAttributes;
|
||||
}
|
||||
|
||||
public async handleService(domain, service, data: Record<string, any>) {
|
||||
@@ -78,7 +54,7 @@ export class Entity {
|
||||
this.lastUpdated = now();
|
||||
this.lastChanged =
|
||||
state === this.state ? this.lastChanged : this.lastUpdated;
|
||||
this.attributes = { ...this.attributes, ...attributes };
|
||||
this.attributes = { ...this.baseAttributes, ...attributes };
|
||||
|
||||
// eslint-disable-next-line
|
||||
console.log("update", this.entityId, this);
|
||||
@@ -92,7 +68,7 @@ export class Entity {
|
||||
return {
|
||||
entity_id: this.entityId,
|
||||
state: this.state,
|
||||
attributes: this.state === "off" ? this.baseAttributes : this.attributes,
|
||||
attributes: this.attributes,
|
||||
last_changed: this.lastChanged,
|
||||
last_updated: this.lastUpdated,
|
||||
};
|
||||
@@ -100,16 +76,6 @@ export class Entity {
|
||||
}
|
||||
|
||||
class LightEntity extends Entity {
|
||||
static CAPABILITY_ATTRIBUTES = new Set([
|
||||
...CAPABILITY_ATTRIBUTES,
|
||||
"min_color_temp_kelvin",
|
||||
"max_color_temp_kelvin",
|
||||
"min_mireds",
|
||||
"max_mireds",
|
||||
"effect_list",
|
||||
"supported_color_modes",
|
||||
]);
|
||||
|
||||
public async handleService(domain, service, data) {
|
||||
if (!["homeassistant", this.domain].includes(domain)) {
|
||||
return;
|
||||
@@ -222,12 +188,6 @@ class AlarmControlPanelEntity extends Entity {
|
||||
}
|
||||
|
||||
class MediaPlayerEntity extends Entity {
|
||||
static CAPABILITY_ATTRIBUTES = new Set([
|
||||
...CAPABILITY_ATTRIBUTES,
|
||||
"source_list",
|
||||
"sound_mode_list",
|
||||
]);
|
||||
|
||||
public async handleService(
|
||||
domain,
|
||||
service,
|
||||
@@ -263,11 +223,7 @@ class CoverEntity extends Entity {
|
||||
if (service === "open_cover") {
|
||||
this.update("open");
|
||||
} else if (service === "close_cover") {
|
||||
this.update("closed");
|
||||
} else if (service === "set_cover_position") {
|
||||
this.update(data.position > 0 ? "open" : "closed", {
|
||||
current_position: data.position,
|
||||
});
|
||||
this.update("closing");
|
||||
} else {
|
||||
super.handleService(domain, service, data);
|
||||
}
|
||||
@@ -332,19 +288,6 @@ class InputSelectEntity extends Entity {
|
||||
}
|
||||
|
||||
class ClimateEntity extends Entity {
|
||||
static CAPABILITY_ATTRIBUTES = new Set([
|
||||
...CAPABILITY_ATTRIBUTES,
|
||||
"hvac_modes",
|
||||
"min_temp",
|
||||
"max_temp",
|
||||
"target_temp_step",
|
||||
"fan_modes",
|
||||
"preset_modes",
|
||||
"swing_modes",
|
||||
"min_humidity",
|
||||
"max_humidity",
|
||||
]);
|
||||
|
||||
public async handleService(domain, service, data) {
|
||||
if (domain !== this.domain) {
|
||||
return;
|
||||
@@ -414,14 +357,6 @@ class ClimateEntity extends Entity {
|
||||
}
|
||||
|
||||
class WaterHeaterEntity extends Entity {
|
||||
static CAPABILITY_ATTRIBUTES = new Set([
|
||||
...CAPABILITY_ATTRIBUTES,
|
||||
"current_temperature",
|
||||
"min_temp",
|
||||
"max_temp",
|
||||
"operation_list",
|
||||
]);
|
||||
|
||||
public async handleService(domain, service, data) {
|
||||
if (domain !== this.domain) {
|
||||
return;
|
||||
@@ -459,7 +394,6 @@ class GroupEntity extends Entity {
|
||||
}
|
||||
|
||||
const TYPES = {
|
||||
automation: ToggleEntity,
|
||||
alarm_control_panel: AlarmControlPanelEntity,
|
||||
climate: ClimateEntity,
|
||||
cover: CoverEntity,
|
||||
|
@@ -278,8 +278,6 @@ export const provideHass = (
|
||||
// @ts-ignore
|
||||
async callService(domain, service, data) {
|
||||
if (data && "entity_id" in data) {
|
||||
// eslint-disable-next-line
|
||||
console.log("Entity service call", domain, service, data);
|
||||
await Promise.all(
|
||||
ensureArray(data.entity_id).map((ent) =>
|
||||
entities[ent].handleService(domain, service, data)
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
mdiFilterVariant,
|
||||
mdiFilterVariantRemove,
|
||||
mdiFormatListChecks,
|
||||
@@ -43,7 +42,6 @@ import "../components/search-input-outlined";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import "./hass-tabs-subpage";
|
||||
import type { PageNavigation } from "./hass-tabs-subpage";
|
||||
import { showDataTableSettingsDialog } from "../components/data-table/show-dialog-data-table-settings";
|
||||
|
||||
@customElement("hass-tabs-subpage-data-table")
|
||||
export class HaTabsSubpageDataTable extends LitElement {
|
||||
@@ -173,10 +171,6 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public groupOrder?: string[];
|
||||
|
||||
@property({ attribute: false }) public columnOrder?: string[];
|
||||
|
||||
@property({ attribute: false }) public hiddenColumns?: string[];
|
||||
|
||||
@state() private _sortColumn?: string;
|
||||
|
||||
@state() private _sortDirection: SortingDirection = null;
|
||||
@@ -296,14 +290,6 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
`
|
||||
: nothing;
|
||||
|
||||
const settingsButton = html`<ha-assist-chip
|
||||
class="has-dropdown select-mode-chip"
|
||||
@click=${this._openSettings}
|
||||
.title=${localize("ui.components.subpage-data-table.settings")}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
</ha-assist-chip>`;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
@@ -430,7 +416,6 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
: ""}
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.columns=${this.columns}
|
||||
.data=${this.data}
|
||||
.noDataText=${this.noDataText}
|
||||
@@ -445,8 +430,6 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.groupColumn=${this._groupColumn}
|
||||
.groupOrder=${this.groupOrder}
|
||||
.initialCollapsedGroups=${this.initialCollapsedGroups}
|
||||
.columnOrder=${this.columnOrder}
|
||||
.hiddenColumns=${this.hiddenColumns}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
@@ -455,7 +438,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
<div class="table-header">
|
||||
${this.hasFilters && !this.showFilters
|
||||
? html`${filterButton}`
|
||||
: nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
|
||||
: nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
@@ -465,7 +448,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
${this.hasFilters && !this.showFilters
|
||||
? html`${filterButton}`
|
||||
: nothing}
|
||||
${selectModeBtn}${groupByMenu}${sortByMenu}${settingsButton}
|
||||
${selectModeBtn}${groupByMenu}${sortByMenu}
|
||||
</div>`}
|
||||
</ha-data-table>`}
|
||||
<div slot="fab"><slot name="fab"></slot></div>
|
||||
@@ -625,22 +608,6 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
fireEvent(this, "grouping-changed", { value: columnId });
|
||||
}
|
||||
|
||||
private _openSettings() {
|
||||
showDataTableSettingsDialog(this, {
|
||||
columns: this.columns,
|
||||
hiddenColumns: this.hiddenColumns,
|
||||
columnOrder: this.columnOrder,
|
||||
onUpdate: (
|
||||
columnOrder: string[] | undefined,
|
||||
hiddenColumns: string[] | undefined
|
||||
) => {
|
||||
this.columnOrder = columnOrder;
|
||||
this.hiddenColumns = hiddenColumns;
|
||||
fireEvent(this, "columns-changed", { columnOrder, hiddenColumns });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _collapseAllGroups() {
|
||||
this._dataTable.collapseAllGroups();
|
||||
}
|
||||
@@ -907,10 +874,6 @@ declare global {
|
||||
interface HASSDomEvents {
|
||||
"search-changed": { value: string };
|
||||
"grouping-changed": { value: string };
|
||||
"columns-changed": {
|
||||
columnOrder: string[] | undefined;
|
||||
hiddenColumns: string[] | undefined;
|
||||
};
|
||||
"clear-filter": undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -72,11 +72,11 @@ class DialogCommunity extends LitElement {
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://x.com/home_assistant"
|
||||
href="https://twitter.com/home_assistant"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img class="x" src="/static/images/logo_x.svg" slot="graphic" />
|
||||
${this.localize("ui.panel.page-onboarding.welcome.x")}
|
||||
<img src="/static/images/logo_twitter.png" slot="graphic" />
|
||||
${this.localize("ui.panel.page-onboarding.welcome.twitter")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
@@ -96,12 +96,6 @@ class DialogCommunity extends LitElement {
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
img.x {
|
||||
filter: invert(1) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
@@ -10,7 +11,6 @@ import "../../../components/ha-combo-box";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-button";
|
||||
import {
|
||||
ApplicationCredential,
|
||||
ApplicationCredentialsConfig,
|
||||
@@ -231,10 +231,10 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-button slot="secondaryAction" @click=${this._abortDialog}>
|
||||
<mwc-button slot="primaryAction" @click=${this._abortDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._domain ||
|
||||
!this._clientId ||
|
||||
@@ -244,7 +244,7 @@ export class DialogAddApplicationCredential extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.editor.add"
|
||||
)}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
|
@@ -7,12 +7,10 @@ import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-help-tooltip";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import {
|
||||
ApplicationCredential,
|
||||
deleteApplicationCredential,
|
||||
@@ -28,7 +26,6 @@ import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showAddApplicationCredentialDialog } from "./show-dialog-add-application-credential";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
@customElement("ha-config-application-credentials")
|
||||
export class HaConfigApplicationCredentials extends LitElement {
|
||||
@@ -47,45 +44,14 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
@storage({
|
||||
key: "application-credentials-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({
|
||||
key: "application-credentials-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "application-credentials-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "application-credentials-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<ApplicationCredential> = {
|
||||
name: {
|
||||
title: localize(
|
||||
"ui.panel.config.application_credentials.picker.headers.name"
|
||||
),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
},
|
||||
@@ -93,41 +59,17 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
title: localize(
|
||||
"ui.panel.config.application_credentials.picker.headers.client_id"
|
||||
),
|
||||
filterable: true,
|
||||
width: "30%",
|
||||
hidden: narrow,
|
||||
},
|
||||
localizedDomain: {
|
||||
title: localize(
|
||||
"ui.panel.config.application_credentials.picker.headers.application"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "30%",
|
||||
direction: "asc",
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
width: "64px",
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
template: (credential) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
.items=${[
|
||||
{
|
||||
path: mdiDelete,
|
||||
warning: true,
|
||||
label: this.hass.localize("ui.common.delete"),
|
||||
action: () => this._deleteCredential(credential),
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
return columns;
|
||||
@@ -156,7 +98,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._getApplicationCredentials(
|
||||
this._applicationCredentials,
|
||||
this.hass.localize
|
||||
@@ -165,18 +107,11 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
>
|
||||
<div class="header-btns" slot="selection-bar">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<mwc-button @click=${this._deleteSelected} class="warning"
|
||||
<mwc-button @click=${this._removeSelected} class="warning"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.application_credentials.picker.remove_selected.button"
|
||||
)}</mwc-button
|
||||
@@ -186,7 +121,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
<ha-icon-button
|
||||
class="warning"
|
||||
id="remove-btn"
|
||||
@click=${this._deleteSelected}
|
||||
@click=${this._removeSelected}
|
||||
.path=${mdiDelete}
|
||||
.label=${this.hass.localize("ui.common.remove")}
|
||||
></ha-icon-button>
|
||||
@@ -218,26 +153,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _deleteCredential = async (credential) => {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.application_credentials.picker.remove.confirm_title`
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.application_credentials.picker.remove_selected.confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
await deleteApplicationCredential(this.hass, credential.id);
|
||||
await this._fetchApplicationCredentials();
|
||||
};
|
||||
|
||||
private _deleteSelected() {
|
||||
private _removeSelected() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.application_credentials.picker.remove_selected.confirm_title`,
|
||||
@@ -246,9 +162,8 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.application_credentials.picker.remove_selected.confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
confirmText: this.hass.localize("ui.common.remove"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
@@ -269,7 +184,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._dataTable.clearSelection();
|
||||
await this._fetchApplicationCredentials();
|
||||
this._fetchApplicationCredentials();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -297,19 +212,6 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.table-header {
|
||||
@@ -359,9 +261,6 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.warning {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -18,7 +18,6 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { stripDiacritics } from "../../../common/string/strip-diacritics";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import "../../../components/ha-dialog";
|
||||
@@ -51,7 +50,6 @@ import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
|
||||
import { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { getStripDiacriticsFn } from "../../../util/fuse";
|
||||
import {
|
||||
AddAutomationElementDialogParams,
|
||||
PASTE_VALUE,
|
||||
@@ -210,10 +208,9 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
getFn: getStripDiacriticsFn,
|
||||
};
|
||||
const fuse = new Fuse(items, options);
|
||||
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
}
|
||||
);
|
||||
|
||||
|
@@ -3,10 +3,10 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-markdown";
|
||||
import { BlueprintAutomationConfig } from "../../../data/automation";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
import "../../../components/ha-markdown";
|
||||
|
||||
@customElement("blueprint-automation-editor")
|
||||
export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
@@ -20,6 +20,14 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.disabled
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.read_only")}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.migrate")}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this.stateObj?.state === "off"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
mdiDebugStepOver,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiFileEdit,
|
||||
mdiInformationOutline,
|
||||
mdiPlay,
|
||||
mdiPlayCircleOutline,
|
||||
@@ -41,12 +40,10 @@ import "../../../components/ha-yaml-editor";
|
||||
import {
|
||||
AutomationConfig,
|
||||
AutomationEntity,
|
||||
BlueprintAutomationConfig,
|
||||
deleteAutomation,
|
||||
fetchAutomationFileConfig,
|
||||
getAutomationEditorInitData,
|
||||
getAutomationStateConfig,
|
||||
normalizeAutomationConfig,
|
||||
saveAutomationConfig,
|
||||
showAutomationEditor,
|
||||
triggerAutomationActions,
|
||||
@@ -68,7 +65,6 @@ import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-a
|
||||
import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename";
|
||||
import "./blueprint-automation-editor";
|
||||
import "./manual-automation-editor";
|
||||
import { substituteBlueprint } from "../../../data/blueprint";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -81,9 +77,9 @@ declare global {
|
||||
unsub?: UnsubscribeFunc;
|
||||
};
|
||||
"ui-mode-not-available": Error;
|
||||
duplicate: undefined;
|
||||
"move-down": undefined;
|
||||
"move-up": undefined;
|
||||
duplicate: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +112,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@state() private _validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
@state() private _blueprintConfig?: BlueprintAutomationConfig;
|
||||
|
||||
private _configSubscriptions: Record<
|
||||
string,
|
||||
(config?: AutomationConfig) => void
|
||||
@@ -202,9 +196,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._promptAutomationAlias}
|
||||
.disabled=${this._readOnly ||
|
||||
!this.automationId ||
|
||||
this._mode === "yaml"}
|
||||
.disabled=${!this.automationId || this._mode === "yaml"}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.rename")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
@@ -228,8 +220,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
: nothing}
|
||||
|
||||
<ha-list-item
|
||||
.disabled=${this._blueprintConfig ||
|
||||
(!this._readOnly && !this.automationId)}
|
||||
.disabled=${!this._readOnly && !this.automationId}
|
||||
graphic="icon"
|
||||
@click=${this._duplicate}
|
||||
>
|
||||
@@ -244,24 +235,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
${useBlueprint
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._takeControl}
|
||||
.disabled=${this._readOnly}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.take_control"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiFileEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-list-item graphic="icon" @click=${this._switchUiMode}>
|
||||
@@ -342,32 +315,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${this._blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.confirm_take_control"
|
||||
)}
|
||||
<div slot="action" style="display: flex;">
|
||||
<mwc-button @click=${this._takeControlSave}
|
||||
>${this.hass.localize("ui.common.yes")}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._revertBlueprint}
|
||||
>${this.hass.localize("ui.common.no")}</mwc-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
? html`<ha-alert alert-type="warning" dismissable
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.read_only"
|
||||
)}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.migrate"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this._mode === "gui"
|
||||
? html`
|
||||
<div
|
||||
@@ -385,6 +332,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
.config=${this._config}
|
||||
.disabled=${Boolean(this._readOnly)}
|
||||
@value-changed=${this._valueChanged}
|
||||
@duplicate=${this._duplicate}
|
||||
></blueprint-automation-editor>
|
||||
`
|
||||
: html`
|
||||
@@ -396,12 +344,25 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
.config=${this._config}
|
||||
.disabled=${Boolean(this._readOnly)}
|
||||
@value-changed=${this._valueChanged}
|
||||
@duplicate=${this._duplicate}
|
||||
></manual-automation-editor>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
? html`${stateObj?.state === "off"
|
||||
? html` ${this._readOnly
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.read_only"
|
||||
)}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.migrate"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${stateObj?.state === "off"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
@@ -426,7 +387,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${classMap({ dirty: !this._readOnly && this._dirty })}
|
||||
class=${classMap({ dirty: this._dirty })}
|
||||
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
|
||||
extended
|
||||
@click=${this._saveAutomation}
|
||||
@@ -471,7 +432,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
this._config = {
|
||||
...baseConfig,
|
||||
...(initData ? normalizeAutomationConfig(initData) : initData),
|
||||
...initData,
|
||||
} as AutomationConfig;
|
||||
this._entityId = undefined;
|
||||
this._readOnly = false;
|
||||
@@ -480,7 +441,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this._config = normalizeAutomationConfig(c.config);
|
||||
this._config = this._normalizeConfig(c.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
this._entityId = this.entityId;
|
||||
@@ -536,6 +497,18 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
private _normalizeConfig(config: AutomationConfig): AutomationConfig {
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
for (const key of ["trigger", "condition", "action"]) {
|
||||
const value = config[key];
|
||||
if (value && !Array.isArray(value)) {
|
||||
config[key] = [value];
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
try {
|
||||
const config = await fetchAutomationFileConfig(
|
||||
@@ -544,7 +517,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
);
|
||||
this._dirty = false;
|
||||
this._readOnly = false;
|
||||
this._config = normalizeAutomationConfig(config);
|
||||
this._config = this._normalizeConfig(config);
|
||||
this._checkValidation();
|
||||
} catch (err: any) {
|
||||
const entityRegistry = await fetchEntityRegistry(this.hass.connection);
|
||||
@@ -665,51 +638,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
};
|
||||
|
||||
private async _takeControl() {
|
||||
const config = this._config as BlueprintAutomationConfig;
|
||||
|
||||
try {
|
||||
const result = await substituteBlueprint(
|
||||
this.hass,
|
||||
"automation",
|
||||
config.use_blueprint.path,
|
||||
config.use_blueprint.input || {}
|
||||
);
|
||||
|
||||
const newConfig = {
|
||||
...normalizeAutomationConfig(result.substituted_config),
|
||||
id: config.id,
|
||||
alias: config.alias,
|
||||
description: config.description,
|
||||
};
|
||||
|
||||
this._blueprintConfig = config;
|
||||
this._config = newConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
}
|
||||
this._readOnly = true;
|
||||
this._errors = undefined;
|
||||
} catch (err: any) {
|
||||
this._errors = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _revertBlueprint() {
|
||||
this._config = this._blueprintConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
}
|
||||
this._blueprintConfig = undefined;
|
||||
this._readOnly = false;
|
||||
}
|
||||
|
||||
private _takeControlSave() {
|
||||
this._readOnly = false;
|
||||
this._dirty = true;
|
||||
this._blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
private async _duplicate() {
|
||||
const result = this._readOnly
|
||||
? await showConfirmationDialog(this, {
|
||||
@@ -842,12 +770,10 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
manual-automation-editor,
|
||||
blueprint-automation-editor,
|
||||
:not(.yaml-mode) > ha-alert {
|
||||
blueprint-automation-editor {
|
||||
margin: 0 auto;
|
||||
max-width: 1040px;
|
||||
padding: 28px 20px 0;
|
||||
display: block;
|
||||
}
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
|
@@ -192,20 +192,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "automation-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "automation-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMenu;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
@@ -265,10 +251,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
const columns: DataTableColumnContainer<AutomationItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.automation.picker.headers.icon"),
|
||||
label: localize("ui.panel.config.automation.picker.headers.state"),
|
||||
type: "icon",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
template: (automation) =>
|
||||
html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
@@ -288,13 +272,30 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
extraTemplate: (automation) =>
|
||||
automation.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${automation.labels}
|
||||
></ha-data-table-labels>`
|
||||
: nothing,
|
||||
template: (automation) => {
|
||||
const date = new Date(automation.attributes.last_triggered);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
<div style="font-size: 14px;">${automation.name}</div>
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
||||
${automation.attributes.last_triggered
|
||||
? dayDifference > 3
|
||||
? formatShortDateTime(date, locale, this.hass.config)
|
||||
: relativeTime(date, locale)
|
||||
: localize("ui.components.relative_time.never")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${automation.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${automation.labels}
|
||||
></ha-data-table-labels>`
|
||||
: nothing}
|
||||
`;
|
||||
},
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.automation.picker.headers.area"),
|
||||
@@ -321,6 +322,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
width: "130px",
|
||||
title: localize("ui.card.automation.last_triggered"),
|
||||
hidden: narrow,
|
||||
template: (automation) => {
|
||||
if (!automation.last_triggered) {
|
||||
return this.hass.localize("ui.components.relative_time.never");
|
||||
@@ -339,9 +341,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
width: "82px",
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
title: "",
|
||||
type: "overflow",
|
||||
hidden: narrow,
|
||||
label: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
template: (automation) => html`
|
||||
<ha-entity-toggle
|
||||
@@ -354,9 +356,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
title: "",
|
||||
width: "64px",
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (automation) => html`
|
||||
<ha-icon-button
|
||||
.automation=${automation}
|
||||
@@ -546,9 +545,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@@ -1419,11 +1415,6 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -38,6 +38,14 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.disabled
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize("ui.panel.config.automation.editor.read_only")}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.migrate")}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this.stateObj?.state === "off"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
@@ -230,6 +238,10 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _duplicate() {
|
||||
fireEvent(this, "duplicate");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -268,6 +280,12 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
font-weight: normal;
|
||||
line-height: 0;
|
||||
}
|
||||
ha-alert.re-order {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -60,19 +60,14 @@ class HaConfigBackup extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: narrow
|
||||
? undefined
|
||||
: (backup) =>
|
||||
html`${backup.name}
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
},
|
||||
path: {
|
||||
title: localize("ui.panel.config.backup.path"),
|
||||
hidden: !narrow,
|
||||
template: (backup) =>
|
||||
html`${backup.name}
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
},
|
||||
size: {
|
||||
title: localize("ui.panel.config.backup.size"),
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||
@@ -81,6 +76,7 @@ class HaConfigBackup extends LitElement {
|
||||
title: localize("ui.panel.config.backup.created"),
|
||||
width: "15%",
|
||||
direction: "desc",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
@@ -91,9 +87,6 @@ class HaConfigBackup extends LitElement {
|
||||
title: "",
|
||||
width: "15%",
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
template: (backup) =>
|
||||
html`<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
|
@@ -3,6 +3,7 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { nestedArrayMove } from "../../../common/util/array-move";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-blueprint-picker";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-circular-progress";
|
||||
@@ -125,14 +126,14 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
);
|
||||
const expanded = !section.collapsed || anyRequired;
|
||||
|
||||
return html`<ha-expansion-panel
|
||||
return html` <ha-expansion-panel
|
||||
outlined
|
||||
.expanded=${expanded}
|
||||
.noCollapse=${anyRequired}
|
||||
>
|
||||
<div slot="header" role="heading" aria-level="3" class="section-header">
|
||||
${section?.icon
|
||||
? html`<ha-icon
|
||||
? html` <ha-icon
|
||||
class="section-header"
|
||||
.icon=${section.icon}
|
||||
></ha-icon>`
|
||||
@@ -260,6 +261,10 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
protected _duplicate() {
|
||||
fireEvent(this, "duplicate");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -313,6 +318,14 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
ha-alert {
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
ha-alert.re-order {
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
overflow: hidden;
|
||||
}
|
||||
div.section-header {
|
||||
display: flex;
|
||||
vertical-align: middle;
|
||||
@@ -320,10 +333,6 @@ export abstract class HaBlueprintGenericEditor extends LitElement {
|
||||
ha-icon.section-header {
|
||||
padding-right: 10px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -107,20 +107,6 @@ class HaBlueprintOverview extends LitElement {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "blueprint-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "blueprint-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "blueprint-table-search",
|
||||
@@ -168,6 +154,8 @@ class HaBlueprintOverview extends LitElement {
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow,
|
||||
_language,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<BlueprintMetaDataPath> => ({
|
||||
name: {
|
||||
@@ -177,12 +165,19 @@ class HaBlueprintOverview extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: narrow
|
||||
? (blueprint) => html`
|
||||
${blueprint.name}<br />
|
||||
<div class="secondary">${blueprint.path}</div>
|
||||
`
|
||||
: undefined,
|
||||
},
|
||||
translated_type: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.type"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
direction: "asc",
|
||||
width: "10%",
|
||||
},
|
||||
@@ -190,6 +185,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.file_name"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
hidden: narrow,
|
||||
direction: "asc",
|
||||
width: "25%",
|
||||
},
|
||||
@@ -201,9 +197,6 @@ class HaBlueprintOverview extends LitElement {
|
||||
title: "",
|
||||
width: this.narrow ? undefined : "10%",
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (blueprint) =>
|
||||
blueprint.error
|
||||
? html`<ha-svg-icon
|
||||
@@ -287,7 +280,11 @@ class HaBlueprintOverview extends LitElement {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
|
||||
id="fullpath"
|
||||
.noDataText=${this.hass.localize(
|
||||
@@ -316,9 +313,6 @@ class HaBlueprintOverview extends LitElement {
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@@ -562,11 +556,6 @@ class HaBlueprintOverview extends LitElement {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyle;
|
||||
}
|
||||
|
@@ -61,32 +61,32 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
|
||||
href="https://community.home-assistant.io"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${hass.localize("ui.panel.config.tips.join_forums")}</a
|
||||
>Forums</a
|
||||
>`,
|
||||
twitter: html`<a
|
||||
href=${documentationUrl(hass, `/twitter`)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${hass.localize("ui.panel.config.tips.join_x")}</a
|
||||
>Twitter</a
|
||||
>`,
|
||||
discord: html`<a
|
||||
href=${documentationUrl(hass, `/join-chat`)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${hass.localize("ui.panel.config.tips.join_chat")}</a
|
||||
>Chat</a
|
||||
>`,
|
||||
blog: html`<a
|
||||
href=${documentationUrl(hass, `/blog`)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${hass.localize("ui.panel.config.tips.join_blog")}</a
|
||||
>Blog</a
|
||||
>`,
|
||||
newsletter: html`<span class="keep-together"
|
||||
><a
|
||||
href="https://newsletter.openhomefoundation.org/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${hass.localize("ui.panel.config.tips.join_newsletter")}</a
|
||||
>Newsletter</a
|
||||
>
|
||||
</span>`,
|
||||
}),
|
||||
|
@@ -154,20 +154,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
@storage({ key: "devices-table-collapsed", state: false, subscribe: false })
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "devices-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "devices-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
@@ -448,13 +434,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
typeof this._devicesAndFilterDomains
|
||||
>["devicesOutput"][number];
|
||||
|
||||
return {
|
||||
const columns: DataTableColumnContainer<DeviceItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.devices.data_table.icon"),
|
||||
type: "icon",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
template: (device) =>
|
||||
device.domains.length
|
||||
? html`<img
|
||||
@@ -469,14 +452,19 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
/>`
|
||||
: "",
|
||||
},
|
||||
name: {
|
||||
};
|
||||
|
||||
if (narrow) {
|
||||
columns.name = {
|
||||
title: localize("ui.panel.config.devices.data_table.device"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
extraTemplate: (device) => html`
|
||||
template: (device) => html`
|
||||
<div style="font-size: 14px;">${device.name}</div>
|
||||
<div class="secondary">${device.area} | ${device.integration}</div>
|
||||
${device.label_entries.length
|
||||
? html`
|
||||
<ha-data-table-labels
|
||||
@@ -485,89 +473,112 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
`
|
||||
: nothing}
|
||||
`,
|
||||
},
|
||||
manufacturer: {
|
||||
title: localize("ui.panel.config.devices.data_table.manufacturer"),
|
||||
};
|
||||
} else {
|
||||
columns.name = {
|
||||
title: localize("ui.panel.config.devices.data_table.device"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "15%",
|
||||
},
|
||||
model: {
|
||||
title: localize("ui.panel.config.devices.data_table.model"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.devices.data_table.area"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "15%",
|
||||
},
|
||||
integration: {
|
||||
title: localize("ui.panel.config.devices.data_table.integration"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "15%",
|
||||
},
|
||||
battery_entity: {
|
||||
title: localize("ui.panel.config.devices.data_table.battery"),
|
||||
showNarrow: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
type: "numeric",
|
||||
width: narrow ? "105px" : "15%",
|
||||
maxWidth: "105px",
|
||||
valueColumn: "battery_level",
|
||||
template: (device) => {
|
||||
const batteryEntityPair = device.battery_entity;
|
||||
const battery =
|
||||
batteryEntityPair && batteryEntityPair[0]
|
||||
? this.hass.states[batteryEntityPair[0]]
|
||||
: undefined;
|
||||
const batteryDomain = battery
|
||||
? computeStateDomain(battery)
|
||||
: undefined;
|
||||
const batteryCharging =
|
||||
batteryEntityPair && batteryEntityPair[1]
|
||||
? this.hass.states[batteryEntityPair[1]]
|
||||
: undefined;
|
||||
|
||||
return battery &&
|
||||
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (device) => html`
|
||||
<div style="font-size: 14px;">${device.name}</div>
|
||||
${device.label_entries.length
|
||||
? html`
|
||||
${batteryDomain === "sensor"
|
||||
? this.hass.formatEntityState(battery)
|
||||
: nothing}
|
||||
<ha-battery-icon
|
||||
.hass=${this.hass}
|
||||
.batteryStateObj=${battery}
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
<ha-data-table-labels
|
||||
.labels=${device.label_entries}
|
||||
></ha-data-table-labels>
|
||||
`
|
||||
: html`—`;
|
||||
},
|
||||
: nothing}
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
columns.manufacturer = {
|
||||
title: localize("ui.panel.config.devices.data_table.manufacturer"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.model = {
|
||||
title: localize("ui.panel.config.devices.data_table.model"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.area = {
|
||||
title: localize("ui.panel.config.devices.data_table.area"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.integration = {
|
||||
title: localize("ui.panel.config.devices.data_table.integration"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "15%",
|
||||
};
|
||||
columns.battery_entity = {
|
||||
title: localize("ui.panel.config.devices.data_table.battery"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
type: "numeric",
|
||||
width: narrow ? "105px" : "15%",
|
||||
maxWidth: "105px",
|
||||
valueColumn: "battery_level",
|
||||
template: (device) => {
|
||||
const batteryEntityPair = device.battery_entity;
|
||||
const battery =
|
||||
batteryEntityPair && batteryEntityPair[0]
|
||||
? this.hass.states[batteryEntityPair[0]]
|
||||
: undefined;
|
||||
const batteryDomain = battery ? computeStateDomain(battery) : undefined;
|
||||
const batteryCharging =
|
||||
batteryEntityPair && batteryEntityPair[1]
|
||||
? this.hass.states[batteryEntityPair[1]]
|
||||
: undefined;
|
||||
|
||||
return battery &&
|
||||
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
|
||||
? html`
|
||||
${batteryDomain === "sensor"
|
||||
? this.hass.formatEntityState(battery)
|
||||
: nothing}
|
||||
<ha-battery-icon
|
||||
.hass=${this.hass}
|
||||
.batteryStateObj=${battery}
|
||||
.batteryChargingStateObj=${batteryCharging}
|
||||
></ha-battery-icon>
|
||||
`
|
||||
: html`—`;
|
||||
},
|
||||
disabled_by: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.devices.data_table.disabled_by"),
|
||||
hidden: true,
|
||||
template: (device) =>
|
||||
device.disabled_by
|
||||
? this.hass.localize("ui.panel.config.devices.disabled")
|
||||
: "",
|
||||
},
|
||||
labels: {
|
||||
title: "",
|
||||
hidden: true,
|
||||
filterable: true,
|
||||
template: (device) =>
|
||||
device.label_entries.map((lbl) => lbl.name).join(" "),
|
||||
},
|
||||
} as DataTableColumnContainer<DeviceItem>;
|
||||
};
|
||||
columns.disabled_by = {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.devices.data_table.disabled_by"),
|
||||
hidden: true,
|
||||
template: (device) =>
|
||||
device.disabled_by
|
||||
? this.hass.localize("ui.panel.config.devices.disabled")
|
||||
: "",
|
||||
};
|
||||
columns.labels = {
|
||||
title: "",
|
||||
hidden: true,
|
||||
filterable: true,
|
||||
template: (device) =>
|
||||
device.label_entries.map((lbl) => lbl.name).join(" "),
|
||||
};
|
||||
|
||||
return columns;
|
||||
});
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
@@ -693,9 +704,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@@ -1035,11 +1043,6 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
|
@@ -186,20 +186,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "entities-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "entities-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
@@ -265,13 +251,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
]);
|
||||
|
||||
private _columns = memoize(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<EntityRow> => ({
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
narrow,
|
||||
_language
|
||||
): DataTableColumnContainer<EntityRow> => ({
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.entities.picker.headers.state_icon"),
|
||||
type: "icon",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
template: (entry) =>
|
||||
entry.icon
|
||||
? html`<ha-icon .icon=${entry.icon}></ha-icon>`
|
||||
@@ -295,23 +283,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
extraTemplate: (entry) =>
|
||||
entry.label_entries.length
|
||||
template: (entry) => html`
|
||||
<div style="font-size: 14px;">${entry.name}</div>
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${entry.entity_id} | ${entry.localized_platform}
|
||||
</div>`
|
||||
: nothing}
|
||||
${entry.label_entries.length
|
||||
? html`
|
||||
<ha-data-table-labels
|
||||
.labels=${entry.label_entries}
|
||||
></ha-data-table-labels>
|
||||
`
|
||||
: nothing,
|
||||
: nothing}
|
||||
`,
|
||||
},
|
||||
entity_id: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.entity_id"),
|
||||
hidden: narrow,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "25%",
|
||||
},
|
||||
localized_platform: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.integration"),
|
||||
hidden: narrow,
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
@@ -327,6 +324,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
area: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.area"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "15%",
|
||||
@@ -345,7 +343,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
status: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.status"),
|
||||
type: "icon",
|
||||
showNarrow: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "68px",
|
||||
@@ -691,7 +688,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.columns=${this._columns(
|
||||
this.hass.localize,
|
||||
this.narrow,
|
||||
this.hass.language
|
||||
)}
|
||||
.data=${filteredEntities}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.search",
|
||||
@@ -713,9 +714,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@@ -1337,11 +1335,6 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -167,20 +167,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@storage({
|
||||
key: "helpers-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "helpers-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@state() private _stateItems: HassEntity[] = [];
|
||||
|
||||
@state() private _entityEntries?: Record<string, EntityRegistryEntry>;
|
||||
@@ -257,13 +243,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<HelperItem> => ({
|
||||
(
|
||||
narrow: boolean,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<HelperItem> => ({
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.helpers.picker.headers.icon"),
|
||||
type: "icon",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
template: (helper) =>
|
||||
helper.entity
|
||||
? html`<ha-state-icon
|
||||
@@ -282,17 +269,23 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
extraTemplate: (helper) =>
|
||||
helper.label_entries.length
|
||||
template: (helper) => html`
|
||||
<div style="font-size: 14px;">${helper.name}</div>
|
||||
${narrow
|
||||
? html`<div class="secondary">${helper.entity_id}</div> `
|
||||
: nothing}
|
||||
${helper.label_entries.length
|
||||
? html`
|
||||
<ha-data-table-labels
|
||||
.labels=${helper.label_entries}
|
||||
></ha-data-table-labels>
|
||||
`
|
||||
: nothing,
|
||||
: nothing}
|
||||
`,
|
||||
},
|
||||
entity_id: {
|
||||
title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
|
||||
hidden: this.narrow,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "25%",
|
||||
@@ -320,9 +313,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
editable: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.helpers.picker.headers.editable"),
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.editable"
|
||||
),
|
||||
type: "icon",
|
||||
showNarrow: true,
|
||||
template: (helper) => html`
|
||||
${!helper.editable
|
||||
? html`
|
||||
@@ -343,12 +337,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
label: "Actions",
|
||||
width: "64px",
|
||||
type: "overflow-menu",
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
template: (helper) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
@@ -566,14 +556,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${helpers}
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@@ -1097,11 +1084,6 @@ ${rejected
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -55,8 +55,6 @@ import {
|
||||
showYamlIntegrationDialog,
|
||||
} from "./show-add-integration-dialog";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import { stripDiacritics } from "../../../common/string/strip-diacritics";
|
||||
import { getStripDiacriticsFn } from "../../../util/fuse";
|
||||
|
||||
export interface IntegrationListItem {
|
||||
name: string;
|
||||
@@ -257,7 +255,6 @@ class AddIntegrationDialog extends LitElement {
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
getFn: getStripDiacriticsFn,
|
||||
};
|
||||
const helpers = Object.entries(h).map(([domain, integration]) => ({
|
||||
domain,
|
||||
@@ -267,16 +264,15 @@ class AddIntegrationDialog extends LitElement {
|
||||
is_built_in: integration.is_built_in !== false,
|
||||
cloud: integration.iot_class?.startsWith("cloud_"),
|
||||
}));
|
||||
const normalizedFilter = stripDiacritics(filter);
|
||||
return [
|
||||
...new Fuse(integrations, options)
|
||||
.search(normalizedFilter)
|
||||
.search(filter)
|
||||
.map((result) => result.item),
|
||||
...new Fuse(yamlIntegrations, options)
|
||||
.search(normalizedFilter)
|
||||
.search(filter)
|
||||
.map((result) => result.item),
|
||||
...new Fuse(helpers, options)
|
||||
.search(normalizedFilter)
|
||||
.search(filter)
|
||||
.map((result) => result.item),
|
||||
];
|
||||
}
|
||||
|
@@ -1,27 +1,25 @@
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { stripDiacritics } from "../../../common/string/strip-diacritics";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button-menu";
|
||||
@@ -31,7 +29,6 @@ import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/search-input";
|
||||
import "../../../components/search-input-outlined";
|
||||
import { ConfigEntry, getConfigEntries } from "../../../data/config_entries";
|
||||
import { getConfigFlowInProgressCollection } from "../../../data/config_flow";
|
||||
import { fetchDiagnosticHandlers } from "../../../data/diagnostics";
|
||||
@@ -40,11 +37,11 @@ import {
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
IntegrationLogInfo,
|
||||
IntegrationManifest,
|
||||
domainToName,
|
||||
fetchIntegrationManifest,
|
||||
fetchIntegrationManifests,
|
||||
IntegrationLogInfo,
|
||||
IntegrationManifest,
|
||||
subscribeLogInfo,
|
||||
} from "../../../data/integration";
|
||||
import {
|
||||
@@ -62,17 +59,18 @@ import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { getStripDiacriticsFn } from "../../../util/fuse";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { isHelperDomain } from "../helpers/const";
|
||||
import "./ha-config-flow-card";
|
||||
import { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||
import "./ha-disabled-config-entry-card";
|
||||
import "./ha-ignored-config-entry-card";
|
||||
import "./ha-integration-card";
|
||||
import type { HaIntegrationCard } from "./ha-integration-card";
|
||||
import "./ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||
import "./ha-disabled-config-entry-card";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/search-input-outlined";
|
||||
|
||||
export interface ConfigEntryExtended extends ConfigEntry {
|
||||
localized_domain_name?: string;
|
||||
@@ -210,12 +208,9 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
getFn: getStripDiacriticsFn,
|
||||
};
|
||||
const fuse = new Fuse(configEntriesInProgress, options);
|
||||
filteredEntries = fuse
|
||||
.search(stripDiacritics(filter))
|
||||
.map((result) => result.item);
|
||||
filteredEntries = fuse.search(filter).map((result) => result.item);
|
||||
} else {
|
||||
filteredEntries = configEntriesInProgress;
|
||||
}
|
||||
|
@@ -205,13 +205,14 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
Search device
|
||||
</mwc-button>`
|
||||
: this._status === "qr_scan"
|
||||
? html` <ha-qr-scanner
|
||||
.hass=${this.hass}
|
||||
? html`${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<ha-qr-scanner
|
||||
.localize=${this.hass.localize}
|
||||
.error=${this._error}
|
||||
@qr-code-scanned=${this._qrCodeScanned}
|
||||
@qr-code-error=${this._qrCodeError}
|
||||
@qr-code-closed=${this._startOver}
|
||||
></ha-qr-scanner>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@@ -360,7 +361,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
</p>
|
||||
</div>
|
||||
${this._supportsSmartStart
|
||||
? html`<div class="outline">
|
||||
? html` <div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code"
|
||||
@@ -497,7 +498,9 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<a
|
||||
href=${`/config/devices/device/${this._device?.id}`}
|
||||
href=${`/config/devices/device/${
|
||||
this._device?.id
|
||||
}`}
|
||||
>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
@@ -596,10 +599,6 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._handleQrCodeScanned(ev.detail.value);
|
||||
}
|
||||
|
||||
private _qrCodeError(ev: CustomEvent): void {
|
||||
this._error = ev.detail.message;
|
||||
}
|
||||
|
||||
private async _handleQrCodeScanned(qrCodeString: string): Promise<void> {
|
||||
this._error = undefined;
|
||||
if (this._status !== "qr_scan" || this._qrProcessing) {
|
||||
|
@@ -66,26 +66,10 @@ export class HaConfigLabels extends LitElement {
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({
|
||||
key: "labels-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "labels-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
|
||||
icon: {
|
||||
title: "",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
label: localize("ui.panel.config.labels.headers.icon"),
|
||||
type: "icon",
|
||||
template: (label) =>
|
||||
@@ -93,7 +77,6 @@ export class HaConfigLabels extends LitElement {
|
||||
},
|
||||
color: {
|
||||
title: "",
|
||||
showNarrow: true,
|
||||
label: localize("ui.panel.config.labels.headers.color"),
|
||||
type: "icon",
|
||||
template: (label) =>
|
||||
@@ -122,9 +105,6 @@ export class HaConfigLabels extends LitElement {
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
width: "64px",
|
||||
type: "overflow-menu",
|
||||
template: (label) => html`
|
||||
@@ -187,9 +167,6 @@ export class HaConfigLabels extends LitElement {
|
||||
.noDataText=${this.hass.localize("ui.panel.config.labels.no_labels")}
|
||||
hasFab
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@@ -320,11 +297,6 @@ export class HaConfigLabels extends LitElement {
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -2,17 +2,21 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiCheckCircleOutline,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiOpenInNew,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiStar,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, PropertyValues, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoize from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
@@ -22,6 +26,9 @@ import "../../../../components/ha-clickable-list-item";
|
||||
import "../../../../components/ha-fab";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-menu";
|
||||
import type { HaMenu } from "../../../../components/ha-menu";
|
||||
import "../../../../components/ha-menu-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { LovelacePanelConfig } from "../../../../data/lovelace";
|
||||
import {
|
||||
@@ -41,13 +48,11 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../../types";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { getLovelaceStrategy } from "../../../lovelace/strategies/get-strategy";
|
||||
import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboard";
|
||||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
type DataTableItem = Pick<
|
||||
LovelaceDashboard,
|
||||
@@ -85,19 +90,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
@state() private _overflowDashboard?: LovelaceDashboard;
|
||||
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMenu;
|
||||
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
@@ -115,8 +110,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
const columns: DataTableColumnContainer<DataTableItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
label: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.icon"
|
||||
),
|
||||
@@ -144,111 +137,118 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: narrow
|
||||
? undefined
|
||||
: (dashboard) => html`
|
||||
${dashboard.title}
|
||||
${dashboard.default
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
style="padding-left: 10px; padding-inline-start: 10px; direction: var(--direction);"
|
||||
.path=${mdiCheckCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<simple-tooltip animation-delay="0">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.default_dashboard`
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
template: (dashboard) => {
|
||||
const titleTemplate = html`
|
||||
${dashboard.title}
|
||||
${dashboard.default
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
style="padding-left: 10px; padding-inline-start: 10px; direction: var(--direction);"
|
||||
.path=${mdiCheckCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<simple-tooltip animation-delay="0">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.default_dashboard`
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
return narrow
|
||||
? html`
|
||||
${titleTemplate}
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
|
||||
)}${dashboard.filename
|
||||
? html` – ${dashboard.filename} `
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: titleTemplate;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
columns.mode = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
template: (dashboard) => html`
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
|
||||
) || dashboard.mode}
|
||||
`,
|
||||
};
|
||||
if (dashboards.some((dashboard) => dashboard.filename)) {
|
||||
columns.filename = {
|
||||
if (!narrow) {
|
||||
columns.mode = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.filename"
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
|
||||
),
|
||||
width: "15%",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
template: (dashboard) => html`
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
|
||||
) || dashboard.mode}
|
||||
`,
|
||||
};
|
||||
if (dashboards.some((dashboard) => dashboard.filename)) {
|
||||
columns.filename = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.filename"
|
||||
),
|
||||
width: "15%",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
};
|
||||
}
|
||||
columns.require_admin = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.require_admin"
|
||||
),
|
||||
sortable: true,
|
||||
type: "icon",
|
||||
width: "100px",
|
||||
template: (dashboard) =>
|
||||
dashboard.require_admin
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: html`—`,
|
||||
};
|
||||
columns.show_in_sidebar = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
|
||||
),
|
||||
type: "icon",
|
||||
width: "121px",
|
||||
template: (dashboard) =>
|
||||
dashboard.show_in_sidebar
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: html`—`,
|
||||
};
|
||||
}
|
||||
columns.require_admin = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.require_admin"
|
||||
),
|
||||
sortable: true,
|
||||
type: "icon",
|
||||
hidden: narrow,
|
||||
width: "100px",
|
||||
template: (dashboard) =>
|
||||
dashboard.require_admin
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: html`—`,
|
||||
};
|
||||
columns.show_in_sidebar = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
|
||||
),
|
||||
type: "icon",
|
||||
hidden: narrow,
|
||||
width: "121px",
|
||||
template: (dashboard) =>
|
||||
dashboard.show_in_sidebar
|
||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||
: html`—`,
|
||||
};
|
||||
|
||||
columns.url_path = {
|
||||
columns.actions = {
|
||||
title: "",
|
||||
label: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.url"
|
||||
),
|
||||
filterable: true,
|
||||
showNarrow: true,
|
||||
width: "100px",
|
||||
template: (dashboard) =>
|
||||
narrow
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiOpenInNew}
|
||||
.urlPath=${dashboard.url_path}
|
||||
@click=${this._navigate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
.urlPath=${dashboard.url_path}
|
||||
@click=${this._navigate}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}</mwc-button
|
||||
>
|
||||
`,
|
||||
width: "64px",
|
||||
type: "icon-button",
|
||||
template: (dashboard) => html`
|
||||
<ha-icon-button
|
||||
.dashboard=${dashboard}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
@click=${this._showOverflowMenu}
|
||||
></ha-icon-button>
|
||||
`,
|
||||
};
|
||||
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
private _showOverflowMenu = (ev) => {
|
||||
if (
|
||||
this._overflowMenu.open &&
|
||||
ev.target === this._overflowMenu.anchorElement
|
||||
) {
|
||||
this._overflowMenu.close();
|
||||
return;
|
||||
}
|
||||
this._overflowDashboard = ev.target.dashboard;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
};
|
||||
|
||||
private _getItems = memoize((dashboards: LovelaceDashboard[]) => {
|
||||
const defaultMode = (
|
||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||
@@ -316,13 +316,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
)}
|
||||
.data=${this._getItems(this._dashboards)}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._editDashboard}
|
||||
@row-click=${this._navigate}
|
||||
id="url_path"
|
||||
hasFab
|
||||
clickable
|
||||
@@ -354,6 +351,22 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-menu-item @click=${this._editDashboard}>
|
||||
<ha-svg-icon .path=${mdiPencil} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">Edit</div>
|
||||
</ha-menu-item>
|
||||
|
||||
<ha-menu-item>
|
||||
<ha-svg-icon .path=${mdiStar} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">Set to default</div>
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item class="warning">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">Delete</div>
|
||||
</ha-menu-item>
|
||||
</ha-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -366,21 +379,23 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._dashboards = await fetchDashboards(this.hass);
|
||||
}
|
||||
|
||||
private _navigate(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
navigate(`/${(ev.target as any).urlPath}`);
|
||||
private _navigate(ev: CustomEvent) {
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
navigate(`/${urlPath}`);
|
||||
}
|
||||
|
||||
private _editDashboard(ev: CustomEvent) {
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
private _editDashboard = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const dashboard = ev.currentTarget.parentElement.anchorElement.automation;
|
||||
|
||||
const urlPath = (ev.currentTarget as any).urlPath;
|
||||
|
||||
if (urlPath === "energy") {
|
||||
navigate("/config/energy");
|
||||
return;
|
||||
}
|
||||
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
|
||||
this._openDetailDialog(dashboard, urlPath);
|
||||
}
|
||||
};
|
||||
|
||||
private async _addDashboard() {
|
||||
showNewDashboardDialog(this, {
|
||||
@@ -475,11 +490,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -67,27 +67,12 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({
|
||||
key: "lovelace-resources-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "lovelace-resources-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
private _columns = memoize(
|
||||
(
|
||||
_language,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<LovelaceResource> => ({
|
||||
url: {
|
||||
main: true,
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.resources.picker.headers.url"
|
||||
),
|
||||
@@ -160,9 +145,6 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
"ui.panel.config.lovelace.resources.picker.no_resources"
|
||||
)}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@@ -284,11 +266,6 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -1,15 +1,12 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../components/entity/ha-entities-picker";
|
||||
import "../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-picture-upload";
|
||||
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import { adminChangeUsername } from "../../../data/auth";
|
||||
import { PersonMutableParams } from "../../../data/person";
|
||||
@@ -140,17 +137,11 @@ class DialogPersonDetail extends LitElement {
|
||||
@change=${this._pictureChanged}
|
||||
></ha-picture-upload>
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.person.detail.allow_login"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.person.detail.allow_login_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-formfield
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.config.person.detail.allow_login"
|
||||
)}${this._user ? ` (${this._user.username})` : ""}`}
|
||||
>
|
||||
<ha-switch
|
||||
@change=${this._allowLoginChanged}
|
||||
.disabled=${this._user &&
|
||||
@@ -159,9 +150,34 @@ class DialogPersonDetail extends LitElement {
|
||||
this._user.is_owner)}
|
||||
.checked=${this._userId}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
</ha-formfield>
|
||||
|
||||
${this._renderUserFields()}
|
||||
${this._user
|
||||
? html`<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.person.detail.local_only"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.person.detail.admin"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.disabled=${this._user.system_generated ||
|
||||
this._user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>`
|
||||
: ""}
|
||||
${this._deviceTrackersAvailable(this.hass)
|
||||
? html`
|
||||
<p>
|
||||
@@ -219,7 +235,7 @@ class DialogPersonDetail extends LitElement {
|
||||
</div>
|
||||
${this._params.entry
|
||||
? html`
|
||||
<ha-button
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click=${this._deleteEntry}
|
||||
@@ -227,10 +243,28 @@ class DialogPersonDetail extends LitElement {
|
||||
this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.person.detail.delete")}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
${this._user && this.hass.user?.is_owner
|
||||
? html`<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._changeUsername}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._changePassword}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
`
|
||||
: nothing}
|
||||
<ha-button
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
@@ -238,96 +272,11 @@ class DialogPersonDetail extends LitElement {
|
||||
${this._params.entry
|
||||
? this.hass!.localize("ui.panel.config.person.detail.update")
|
||||
: this.hass!.localize("ui.panel.config.person.detail.create")}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderUserFields() {
|
||||
const user = this._user;
|
||||
if (!user) return nothing;
|
||||
return html`
|
||||
${!user.system_generated
|
||||
? html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize("ui.panel.config.person.detail.username")}
|
||||
</span>
|
||||
<span slot="description">${user.username}</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changeUsername}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.person.detail.change_username"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-settings-row>
|
||||
`
|
||||
: nothing}
|
||||
${!user.system_generated && this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize("ui.panel.config.person.detail.password")}
|
||||
</span>
|
||||
<span slot="description">************</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changePassword}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.person.detail.change_password"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-settings-row>
|
||||
`
|
||||
: nothing}
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.person.detail.local_access_only"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.person.detail.local_access_only_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated}
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize("ui.panel.config.person.detail.admin")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.person.detail.admin_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
`;
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
this._params = undefined;
|
||||
}
|
||||
@@ -368,16 +317,14 @@ class DialogPersonDetail extends LitElement {
|
||||
} else if (this._userId) {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.person.detail.confirm_delete_user_title"
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.person.detail.confirm_delete_user_text",
|
||||
"ui.panel.config.person.detail.confirm_delete_user",
|
||||
{ name: this._name }
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
confirmText: this.hass!.localize(
|
||||
"ui.panel.config.person.detail.delete"
|
||||
),
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
target.checked = true;
|
||||
@@ -541,8 +488,9 @@ class DialogPersonDetail extends LitElement {
|
||||
margin-bottom: 16px;
|
||||
--file-upload-image-border-radius: 50%;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
ha-formfield {
|
||||
display: block;
|
||||
padding: 16px 0;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
|
@@ -180,20 +180,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "scene-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "scene-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
@@ -239,13 +225,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
(narrow, localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<SceneItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.scene.picker.headers.icon"),
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
label: localize("ui.panel.config.scene.picker.headers.state"),
|
||||
type: "icon",
|
||||
template: (scene) => html`
|
||||
<ha-state-icon
|
||||
@@ -261,13 +245,15 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
extraTemplate: (scene) =>
|
||||
scene.labels.length
|
||||
template: (scene) => html`
|
||||
<div style="font-size: 14px;">${scene.name}</div>
|
||||
${scene.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${scene.labels}
|
||||
></ha-data-table-labels>`
|
||||
: nothing,
|
||||
: nothing}
|
||||
`,
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.scene.picker.headers.area"),
|
||||
@@ -295,6 +281,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
sortable: true,
|
||||
width: "30%",
|
||||
hidden: narrow,
|
||||
template: (scene) => {
|
||||
const lastActivated = scene.state;
|
||||
if (!lastActivated || isUnavailableState(lastActivated)) {
|
||||
@@ -313,7 +300,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
only_editable: {
|
||||
title: "",
|
||||
width: "56px",
|
||||
showNarrow: true,
|
||||
template: (scene) =>
|
||||
!scene.attributes.id
|
||||
? html`
|
||||
@@ -333,9 +319,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
title: "",
|
||||
width: "64px",
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (scene) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
@@ -553,14 +536,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
id="entity_id"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@@ -1175,11 +1155,6 @@ ${rejected
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-markdown";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import "../../../components/ha-alert";
|
||||
import { BlueprintScriptConfig } from "../../../data/script";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
import "../../../components/ha-markdown";
|
||||
|
||||
@customElement("blueprint-script-editor")
|
||||
export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
@@ -16,6 +17,14 @@ export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.disabled
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize("ui.panel.config.script.editor.read_only")}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize("ui.panel.config.script.editor.migrate")}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this.config.description
|
||||
? html`<ha-markdown
|
||||
class="description"
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
mdiDebugStepOver,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiFileEdit,
|
||||
mdiFormTextbox,
|
||||
mdiInformationOutline,
|
||||
mdiPlay,
|
||||
@@ -41,7 +40,6 @@ import { validateConfig } from "../../../data/config";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import {
|
||||
BlueprintScriptConfig,
|
||||
ScriptConfig,
|
||||
deleteScript,
|
||||
fetchScriptFileConfig,
|
||||
@@ -63,7 +61,6 @@ import { showAutomationRenameDialog } from "../automation/automation-rename-dial
|
||||
import "./blueprint-script-editor";
|
||||
import "./manual-script-editor";
|
||||
import type { HaManualScriptEditor } from "./manual-script-editor";
|
||||
import { substituteBlueprint } from "../../../data/blueprint";
|
||||
|
||||
export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -99,8 +96,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@state() private _validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
@state() private _blueprintConfig?: BlueprintScriptConfig;
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
@@ -218,8 +213,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
: nothing}
|
||||
|
||||
<ha-list-item
|
||||
.disabled=${this._blueprintConfig ||
|
||||
(!this._readOnly && !this.scriptId)}
|
||||
.disabled=${!this._readOnly && !this.scriptId}
|
||||
graphic="icon"
|
||||
@click=${this._duplicate}
|
||||
>
|
||||
@@ -234,24 +228,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
|
||||
${useBlueprint
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._takeControl}
|
||||
.disabled=${this._readOnly}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.take_control"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiFileEdit}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<li divider role="separator"></li>
|
||||
|
||||
<ha-list-item graphic="icon" @click=${this._switchUiMode}>
|
||||
@@ -315,32 +291,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
${this._blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.confirm_take_control"
|
||||
)}
|
||||
<div slot="action" style="display: flex;">
|
||||
<mwc-button @click=${this._takeControlSave}
|
||||
>${this.hass.localize("ui.common.yes")}</mwc-button
|
||||
>
|
||||
<mwc-button @click=${this._revertBlueprint}
|
||||
>${this.hass.localize("ui.common.no")}</mwc-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
? html`<ha-alert alert-type="warning" dismissable
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.script.editor.read_only"
|
||||
)}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.migrate"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this._mode === "gui"
|
||||
? html`
|
||||
<div
|
||||
@@ -357,6 +307,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
@value-changed=${this._valueChanged}
|
||||
@duplicate=${this._duplicate}
|
||||
></blueprint-script-editor>
|
||||
`
|
||||
: html`
|
||||
@@ -367,18 +318,31 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
@value-changed=${this._valueChanged}
|
||||
@duplicate=${this._duplicate}
|
||||
></manual-script-editor>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
? html`<ha-yaml-editor
|
||||
copyClipboard
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
@value-changed=${this._yamlChanged}
|
||||
></ha-yaml-editor>`
|
||||
? html` ${this._readOnly
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.read_only"
|
||||
)}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.migrate"
|
||||
)}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
<ha-yaml-editor
|
||||
copyClipboard
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
@value-changed=${this._yamlChanged}
|
||||
></ha-yaml-editor>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-fab
|
||||
@@ -637,50 +601,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
};
|
||||
|
||||
private async _takeControl() {
|
||||
const config = this._config as BlueprintScriptConfig;
|
||||
|
||||
try {
|
||||
const result = await substituteBlueprint(
|
||||
this.hass,
|
||||
"script",
|
||||
config.use_blueprint.path,
|
||||
config.use_blueprint.input || {}
|
||||
);
|
||||
|
||||
const newConfig = {
|
||||
...this._normalizeConfig(result.substituted_config),
|
||||
alias: config.alias,
|
||||
description: config.description,
|
||||
};
|
||||
|
||||
this._blueprintConfig = config;
|
||||
this._config = newConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
}
|
||||
this._readOnly = true;
|
||||
this._errors = undefined;
|
||||
} catch (err: any) {
|
||||
this._errors = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _revertBlueprint() {
|
||||
this._config = this._blueprintConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
}
|
||||
this._blueprintConfig = undefined;
|
||||
this._readOnly = false;
|
||||
}
|
||||
|
||||
private _takeControlSave() {
|
||||
this._readOnly = false;
|
||||
this._dirty = true;
|
||||
this._blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
private async _duplicate() {
|
||||
const result = this._readOnly
|
||||
? await showConfirmationDialog(this, {
|
||||
@@ -832,12 +752,10 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
.config-container,
|
||||
manual-script-editor,
|
||||
blueprint-script-editor,
|
||||
:not(.yaml-mode) > ha-alert {
|
||||
blueprint-script-editor {
|
||||
margin: 0 auto;
|
||||
max-width: 1040px;
|
||||
padding: 28px 20px 0;
|
||||
display: block;
|
||||
}
|
||||
.config-container ha-alert {
|
||||
margin-bottom: 16px;
|
||||
|
@@ -184,20 +184,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "script-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "script-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
@@ -246,13 +232,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<ScriptItem> => {
|
||||
(
|
||||
narrow,
|
||||
localize: LocalizeFunc,
|
||||
locale: HomeAssistant["locale"]
|
||||
): DataTableColumnContainer<ScriptItem> => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
label: localize("ui.panel.config.script.picker.headers.icon"),
|
||||
label: localize("ui.panel.config.script.picker.headers.state"),
|
||||
type: "icon",
|
||||
template: (script) =>
|
||||
html`<ha-state-icon
|
||||
@@ -271,13 +259,30 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
extraTemplate: (script) =>
|
||||
script.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${script.labels}
|
||||
></ha-data-table-labels>`
|
||||
: nothing,
|
||||
template: (script) => {
|
||||
const date = new Date(script.last_triggered);
|
||||
const now = new Date();
|
||||
const dayDifference = differenceInDays(now, date);
|
||||
return html`
|
||||
<div style="font-size: 14px;">${script.name}</div>
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
||||
${script.attributes.last_triggered
|
||||
? dayDifference > 3
|
||||
? formatShortDateTime(date, locale, this.hass.config)
|
||||
: relativeTime(date, locale)
|
||||
: localize("ui.components.relative_time.never")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${script.labels.length
|
||||
? html`<ha-data-table-labels
|
||||
@label-clicked=${this._labelClicked}
|
||||
.labels=${script.labels}
|
||||
></ha-data-table-labels>`
|
||||
: nothing}
|
||||
`;
|
||||
},
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.script.picker.headers.area"),
|
||||
@@ -300,6 +305,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
template: (script) => script.labels.map((lbl) => lbl.name).join(" "),
|
||||
},
|
||||
last_triggered: {
|
||||
hidden: narrow,
|
||||
sortable: true,
|
||||
width: "40%",
|
||||
title: localize("ui.card.automation.last_triggered"),
|
||||
@@ -324,9 +330,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
title: "",
|
||||
width: "64px",
|
||||
type: "overflow-menu",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
template: (script) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
@@ -536,9 +539,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@@ -553,7 +553,11 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.localize,
|
||||
this.hass.locale
|
||||
)}
|
||||
.data=${scripts}
|
||||
.empty=${!this.scripts.length}
|
||||
.activeFilters=${this._activeFilters}
|
||||
@@ -1266,11 +1270,6 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -60,6 +60,14 @@ export class HaManualScriptEditor extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.disabled
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize("ui.panel.config.script.editor.read_only")}
|
||||
<mwc-button slot="action" @click=${this._duplicate}>
|
||||
${this.hass.localize("ui.panel.config.script.editor.migrate")}
|
||||
</mwc-button>
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this.config.description
|
||||
? html`<ha-markdown
|
||||
class="description"
|
||||
@@ -162,6 +170,10 @@ export class HaManualScriptEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _duplicate() {
|
||||
fireEvent(this, "duplicate");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
@@ -193,6 +205,12 @@ export class HaManualScriptEditor extends LitElement {
|
||||
.header a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-alert.re-order {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
overflow: hidden;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -66,82 +66,93 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<TagRowData> = {
|
||||
icon: {
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, _language, localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<TagRowData> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.tag.headers.icon"),
|
||||
type: "icon",
|
||||
template: (tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
||||
},
|
||||
display_name: {
|
||||
title: localize("ui.panel.config.tag.headers.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (tag) =>
|
||||
html`${tag.display_name}
|
||||
${narrow
|
||||
? html`<div class="secondary">
|
||||
${tag.last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${tag.last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tag.never_scanned")}
|
||||
</div>`
|
||||
: ""}`,
|
||||
},
|
||||
last_scanned_datetime: {
|
||||
title: localize("ui.panel.config.tag.headers.last_scanned"),
|
||||
sortable: true,
|
||||
hidden: narrow,
|
||||
direction: "desc",
|
||||
width: "20%",
|
||||
template: (tag) => html`
|
||||
${tag.last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${tag.last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tag.never_scanned")}
|
||||
`,
|
||||
},
|
||||
};
|
||||
if (this._canWriteTags) {
|
||||
columns.write = {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.tag.headers.write"),
|
||||
type: "icon-button",
|
||||
template: (tag) =>
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleWriteClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.write")}
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
}
|
||||
columns.automation = {
|
||||
title: "",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
label: localize("ui.panel.config.tag.headers.icon"),
|
||||
type: "icon",
|
||||
template: (tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
||||
},
|
||||
display_name: {
|
||||
title: localize("ui.panel.config.tag.headers.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
},
|
||||
last_scanned_datetime: {
|
||||
title: localize("ui.panel.config.tag.headers.last_scanned"),
|
||||
sortable: true,
|
||||
direction: "desc",
|
||||
width: "20%",
|
||||
template: (tag) => html`
|
||||
${tag.last_scanned_datetime
|
||||
? html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${tag.last_scanned_datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`
|
||||
: this.hass.localize("ui.panel.config.tag.never_scanned")}
|
||||
`,
|
||||
},
|
||||
};
|
||||
if (this._canWriteTags) {
|
||||
columns.write = {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.tag.headers.write"),
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
template: (tag) =>
|
||||
html`<ha-icon-button
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleWriteClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.write")}
|
||||
.path=${mdiContentDuplicate}
|
||||
@click=${this._handleAutomationClick}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.tag.create_automation"
|
||||
)}
|
||||
.path=${mdiRobot}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
columns.edit = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
template: (tag) =>
|
||||
html` <ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleEditClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.edit")}
|
||||
.path=${mdiCog}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
return columns;
|
||||
}
|
||||
columns.automation = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
template: (tag) =>
|
||||
html`<ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleAutomationClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.create_automation")}
|
||||
.path=${mdiRobot}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
columns.edit = {
|
||||
title: "",
|
||||
type: "icon-button",
|
||||
showNarrow: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
template: (tag) =>
|
||||
html`<ha-icon-button
|
||||
.tag=${tag}
|
||||
@click=${this._handleEditClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.edit")}
|
||||
.path=${mdiCog}
|
||||
></ha-icon-button>`,
|
||||
};
|
||||
return columns;
|
||||
});
|
||||
);
|
||||
|
||||
private _data = memoizeOne((tags: Tag[]): TagRowData[] =>
|
||||
tags.map((tag) => ({
|
||||
@@ -180,7 +191,11 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.tags}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.data=${this._data(this._tags)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")}
|
||||
.filter=${this._filter}
|
||||
|
@@ -1,34 +1,31 @@
|
||||
import "@material/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-switch";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import { createAuthForUser } from "../../../data/auth";
|
||||
import {
|
||||
createUser,
|
||||
deleteUser,
|
||||
SYSTEM_GROUP_ID_ADMIN,
|
||||
SYSTEM_GROUP_ID_USER,
|
||||
User,
|
||||
createUser,
|
||||
deleteUser,
|
||||
} from "../../../data/user";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../../../types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { AddUserDialogParams } from "./show-dialog-add-user";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
|
||||
@customElement("dialog-add-user")
|
||||
export class DialogAddUser extends LitElement {
|
||||
@@ -158,44 +155,38 @@ export class DialogAddUser extends LitElement {
|
||||
"ui.panel.config.users.add_user.password_not_match"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only"
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_only"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize("ui.panel.config.users.editor.admin")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch .checked=${this._isAdmin} @change=${this._adminChanged}>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-switch
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
|
||||
>
|
||||
<ha-switch
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${!this._isAdmin
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
</ha-alert>
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
`
|
||||
: nothing}
|
||||
: ""}
|
||||
</div>
|
||||
${this._loading
|
||||
? html`
|
||||
@@ -204,7 +195,7 @@ export class DialogAddUser extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-button
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._name ||
|
||||
!this._username ||
|
||||
@@ -213,7 +204,7 @@ export class DialogAddUser extends LitElement {
|
||||
@click=${this._createUser}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.users.add_user.create")}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -290,11 +281,6 @@ export class DialogAddUser extends LitElement {
|
||||
}
|
||||
|
||||
user.username = this._username;
|
||||
user.credentials = [
|
||||
{
|
||||
type: "homeassistant",
|
||||
},
|
||||
];
|
||||
this._params!.userAddedCallback(user);
|
||||
this._close();
|
||||
}
|
||||
@@ -313,10 +299,7 @@ export class DialogAddUser extends LitElement {
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -132,7 +132,7 @@ class DialogAdminChangePassword extends LitElement {
|
||||
@value-changed=${this._valueChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-form>
|
||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
|
@@ -1,13 +1,12 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-help-tooltip";
|
||||
import "../../../components/ha-label";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
@@ -69,15 +68,15 @@ class DialogUserDetail extends LitElement {
|
||||
.heading=${createCloseHeading(this.hass, user.name)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<div class="error">${this._error}</div>`
|
||||
: nothing}
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
<div class="secondary">
|
||||
${this.hass.localize("ui.panel.config.users.editor.id")}:
|
||||
${user.id}<br />
|
||||
${this.hass.localize("ui.panel.config.users.editor.username")}:
|
||||
${user.username}
|
||||
</div>
|
||||
${badges.length === 0
|
||||
? nothing
|
||||
? ""
|
||||
: html`
|
||||
<div class="badge-container">
|
||||
${badges.map(
|
||||
@@ -91,136 +90,74 @@ class DialogUserDetail extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
<div class="form">
|
||||
${!user.system_generated
|
||||
? html`
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.users.editor.name"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.username"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">${user.username}</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changeUsername}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-settings-row>
|
||||
`
|
||||
: nothing}
|
||||
${!user.system_generated && this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.password"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">************</span>
|
||||
${this.hass.user?.is_owner
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiPencil}
|
||||
@click=${this._changePassword}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-settings-row>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize("ui.panel.config.users.editor.active")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active_description"
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
.value=${this._name}
|
||||
.disabled=${user.system_generated}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass!.localize("ui.panel.config.users.editor.name")}
|
||||
></ha-textfield>
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_only"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isActive}
|
||||
@change=${this._activeChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only"
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated}
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated}
|
||||
.checked=${this._localOnly}
|
||||
@change=${this._localOnlyChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize("ui.panel.config.users.editor.admin")}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.admin_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
${!this._isAdmin && !user.system_generated
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${!this._isAdmin
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${user.system_generated
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated_read_only_users"
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: ""}
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isActive}
|
||||
@change=${this._activeChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-help-tooltip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active_tooltip"
|
||||
)}
|
||||
>
|
||||
</ha-help-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div slot="secondaryAction">
|
||||
<ha-button
|
||||
<mwc-button
|
||||
class="warning"
|
||||
@click=${this._deleteEntry}
|
||||
.disabled=${this._submitting ||
|
||||
@@ -228,18 +165,47 @@ class DialogUserDetail extends LitElement {
|
||||
user.is_owner}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.users.editor.delete_user")}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
${user.system_generated
|
||||
? html`
|
||||
<simple-tooltip animation-delay="0" position="right">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated_users_not_removable"
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
${!user.system_generated && this.hass.user?.is_owner
|
||||
? html`<mwc-button @click=${this._changeUsername}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)} </mwc-button
|
||||
><mwc-button @click=${this._changePassword}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
<div slot="primaryAction">
|
||||
<ha-button
|
||||
<mwc-button
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!this._name ||
|
||||
this._submitting ||
|
||||
user.system_generated}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.users.editor.update_user")}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
${user.system_generated
|
||||
? html`
|
||||
<simple-tooltip animation-delay="0" position="left">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated_users_not_editable"
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -387,8 +353,27 @@ class DialogUserDetail extends LitElement {
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
.state {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.15);
|
||||
border-radius: 16px;
|
||||
padding: 4px 8px;
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
.state:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
}
|
||||
ha-help-tooltip {
|
||||
margin-left: 4px;
|
||||
margin-inline-start: 4px;
|
||||
margin-inline-end: initial;
|
||||
position: relative;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -46,20 +46,6 @@ export class HaConfigUsers extends LitElement {
|
||||
@storage({ key: "users-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "users-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "users-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "users-table-search",
|
||||
@@ -86,6 +72,17 @@ export class HaConfigUsers extends LitElement {
|
||||
width: "25%",
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (user) =>
|
||||
narrow
|
||||
? html` ${user.name}<br />
|
||||
<div class="secondary">
|
||||
${user.username ? `${user.username} |` : ""}
|
||||
${localize(`groups.${user.group_ids[0]}`)}
|
||||
</div>`
|
||||
: html` ${user.name ||
|
||||
this.hass!.localize(
|
||||
"ui.panel.config.users.editor.unnamed_user"
|
||||
)}`,
|
||||
},
|
||||
username: {
|
||||
title: localize("ui.panel.config.users.picker.headers.username"),
|
||||
@@ -93,6 +90,7 @@ export class HaConfigUsers extends LitElement {
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
template: (user) => html`${user.username || "—"}`,
|
||||
},
|
||||
group: {
|
||||
@@ -102,6 +100,7 @@ export class HaConfigUsers extends LitElement {
|
||||
groupable: true,
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
},
|
||||
is_active: {
|
||||
title: this.hass.localize(
|
||||
@@ -155,7 +154,6 @@ export class HaConfigUsers extends LitElement {
|
||||
filterable: false,
|
||||
width: "104px",
|
||||
hidden: !narrow,
|
||||
showNarrow: true,
|
||||
template: (user) => {
|
||||
const badges = computeUserBadges(this.hass, user, false);
|
||||
return html`${badges.map(
|
||||
@@ -188,9 +186,6 @@ export class HaConfigUsers extends LitElement {
|
||||
.tabs=${configSections.persons}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._userData(this._users, this.hass.localize)}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@@ -218,7 +213,6 @@ export class HaConfigUsers extends LitElement {
|
||||
private _userData = memoizeOne((users: User[], localize: LocalizeFunc) =>
|
||||
users.map((user) => ({
|
||||
...user,
|
||||
name: user.name || localize("ui.panel.config.users.editor.unnamed_user"),
|
||||
group: localize(`groups.${user.group_ids[0]}`),
|
||||
}))
|
||||
);
|
||||
@@ -308,11 +302,6 @@ export class HaConfigUsers extends LitElement {
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -118,20 +118,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
key: "voice-expose-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@storage({
|
||||
key: "voice-expose-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
@@ -151,7 +137,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
moveable: false,
|
||||
hidden: narrow,
|
||||
template: (entry) => html`
|
||||
<ha-state-icon
|
||||
@@ -168,20 +153,10 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: narrow
|
||||
? undefined
|
||||
: (entry) => html`
|
||||
${entry.name}<br />
|
||||
<div class="secondary">${entry.entity_id}</div>
|
||||
`,
|
||||
},
|
||||
// For search & narrow
|
||||
entity_id: {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.entity_id"
|
||||
),
|
||||
hidden: !narrow,
|
||||
filterable: true,
|
||||
template: (entry) => html`
|
||||
${entry.name}<br />
|
||||
<div class="secondary">${entry.entity_id}</div>
|
||||
`,
|
||||
},
|
||||
domain: {
|
||||
title: localize(
|
||||
@@ -196,6 +171,7 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
title: localize("ui.panel.config.voice_assistants.expose.headers.area"),
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
width: "15%",
|
||||
},
|
||||
@@ -203,7 +179,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
showNarrow: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "160px",
|
||||
@@ -233,6 +208,7 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
hidden: narrow,
|
||||
width: "15%",
|
||||
template: (entry) =>
|
||||
entry.aliases.length === 0
|
||||
@@ -254,6 +230,12 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
.path=${mdiCloseCircleOutline}
|
||||
></ha-icon-button>`,
|
||||
},
|
||||
// For search
|
||||
entity_id: {
|
||||
title: "",
|
||||
hidden: true,
|
||||
filterable: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -570,9 +552,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
.initialSorting=${this._activeSorting}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@@ -778,11 +757,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -139,7 +139,7 @@ export class HaLogbook extends LitElement {
|
||||
this._throttleGetLogbookEntries.cancel();
|
||||
this._updateTraceContexts.cancel();
|
||||
this._updateUsers.cancel();
|
||||
this._unsubscribeSetLoading();
|
||||
await this._unsubscribeSetLoading();
|
||||
|
||||
if (force) {
|
||||
this._getLogBookData();
|
||||
@@ -206,9 +206,18 @@ export class HaLogbook extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _unsubscribe() {
|
||||
private async _unsubscribe(): Promise<void> {
|
||||
if (this._subscribed) {
|
||||
this._subscribed.then((unsub) => unsub?.());
|
||||
const unsub = await this._subscribed;
|
||||
if (unsub) {
|
||||
try {
|
||||
await unsub();
|
||||
} catch (e) {
|
||||
// The backend will cancel the subscription if
|
||||
// we subscribe to entities that will all be
|
||||
// filtered away
|
||||
}
|
||||
}
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
}
|
||||
@@ -230,8 +239,8 @@ export class HaLogbook extends LitElement {
|
||||
* Setting this._logbookEntries to undefined
|
||||
* will put the page in a loading state.
|
||||
*/
|
||||
private _unsubscribeSetLoading() {
|
||||
this._unsubscribe();
|
||||
private async _unsubscribeSetLoading() {
|
||||
await this._unsubscribe();
|
||||
this._logbookEntries = undefined;
|
||||
this._pendingStreamMessages = [];
|
||||
}
|
||||
@@ -240,8 +249,8 @@ export class HaLogbook extends LitElement {
|
||||
* Setting this._logbookEntries to an empty
|
||||
* list will show a no results message.
|
||||
*/
|
||||
private _unsubscribeNoResults() {
|
||||
this._unsubscribe();
|
||||
private async _unsubscribeNoResults() {
|
||||
await this._unsubscribe();
|
||||
this._logbookEntries = [];
|
||||
this._pendingStreamMessages = [];
|
||||
}
|
||||
|
@@ -55,11 +55,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../components/hui-image";
|
||||
import "../components/hui-warning";
|
||||
import {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { AreaCardConfig } from "./types";
|
||||
|
||||
export const DEFAULT_ASPECT_RATIO = "16:9";
|
||||
@@ -106,9 +102,6 @@ export class HuiAreaCard
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public layout?: string;
|
||||
|
||||
@state() private _config?: AreaCardConfig;
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
@@ -414,17 +407,13 @@ export class HuiAreaCard
|
||||
}
|
||||
|
||||
const imageClass = area.picture || cameraEntityId;
|
||||
|
||||
const ignoreAspectRatio = this.layout === "grid";
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${imageClass ? "image" : ""}
|
||||
style=${styleMap({
|
||||
paddingBottom:
|
||||
ignoreAspectRatio || imageClass
|
||||
? "0"
|
||||
: `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%`,
|
||||
paddingBottom: imageClass
|
||||
? "0"
|
||||
: `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%`,
|
||||
})}
|
||||
>
|
||||
${area.picture || cameraEntityId
|
||||
@@ -435,10 +424,8 @@ export class HuiAreaCard
|
||||
.image=${area.picture ? area.picture : undefined}
|
||||
.cameraImage=${cameraEntityId}
|
||||
.cameraView=${this._config.camera_view}
|
||||
.aspectRatio=${ignoreAspectRatio
|
||||
? undefined
|
||||
: this._config.aspect_ratio || DEFAULT_ASPECT_RATIO}
|
||||
fitMode="cover"
|
||||
.aspectRatio=${this._config.aspect_ratio ||
|
||||
DEFAULT_ASPECT_RATIO}
|
||||
></hui-image>
|
||||
`
|
||||
: area.icon
|
||||
@@ -547,20 +534,12 @@ export class HuiAreaCard
|
||||
forwardHaptic("light");
|
||||
}
|
||||
|
||||
getLayoutOptions(): LovelaceLayoutOptions {
|
||||
return {
|
||||
grid_columns: 4,
|
||||
grid_rows: 3,
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -588,10 +567,6 @@ export class HuiAreaCard
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
.image hui-image {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@@ -145,16 +145,9 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
this._config?.show_icon &&
|
||||
(this._config?.show_name || this._config?.show_state)
|
||||
) {
|
||||
return {
|
||||
grid_rows: 2,
|
||||
grid_columns: 2,
|
||||
grid_min_rows: 2,
|
||||
};
|
||||
return { grid_rows: 2, grid_columns: 2 };
|
||||
}
|
||||
return {
|
||||
grid_rows: 1,
|
||||
grid_columns: 1,
|
||||
};
|
||||
return { grid_rows: 1, grid_columns: 1 };
|
||||
}
|
||||
|
||||
public setConfig(config: ButtonCardConfig): void {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { PropertyValueMap, PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||
@@ -23,25 +23,30 @@ declare global {
|
||||
|
||||
@customElement("hui-card")
|
||||
export class HuiCard extends ReactiveElement {
|
||||
@property({ attribute: false }) public preview = false;
|
||||
|
||||
@property({ attribute: false }) public isPanel = false;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceCardConfig;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public layout?: string;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
private _elementConfig?: LovelaceCardConfig;
|
||||
@property({ type: Boolean }) public isPanel = false;
|
||||
|
||||
public load() {
|
||||
if (!this.config) {
|
||||
throw new Error("Cannot build card without config");
|
||||
set config(config: LovelaceCardConfig | undefined) {
|
||||
if (!config) return;
|
||||
if (config.type !== this._config?.type) {
|
||||
this._buildElement(config);
|
||||
} else if (config !== this.config) {
|
||||
this._element?.setConfig(config);
|
||||
fireEvent(this, "card-updated");
|
||||
}
|
||||
this._loadElement(this.config);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
public get config() {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
private _config?: LovelaceCardConfig;
|
||||
|
||||
private _element?: LovelaceCard;
|
||||
|
||||
private _listeners: MediaQueriesListener[] = [];
|
||||
@@ -85,86 +90,55 @@ export class HuiCard extends ReactiveElement {
|
||||
return this._element?.getLayoutOptions?.() ?? {};
|
||||
}
|
||||
|
||||
private _updateElement(config: LovelaceCardConfig) {
|
||||
if (!this._element) {
|
||||
return;
|
||||
}
|
||||
this._element.setConfig(config);
|
||||
this._elementConfig = config;
|
||||
fireEvent(this, "card-updated");
|
||||
}
|
||||
|
||||
private _loadElement(config: LovelaceCardConfig) {
|
||||
this._element = createCardElement(config);
|
||||
this._elementConfig = config;
|
||||
if (this.hass) {
|
||||
this._element.hass = this.hass;
|
||||
}
|
||||
this._element.layout = this.layout;
|
||||
this._element.preview = this.preview;
|
||||
private _createElement(config: LovelaceCardConfig) {
|
||||
const element = createCardElement(config);
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
// For backwards compatibility
|
||||
(this._element as any).editMode = this.preview;
|
||||
(element as any).editMode = this.preview;
|
||||
// Update element when the visibility of the card changes (e.g. conditional card or filter card)
|
||||
this._element.addEventListener("card-visibility-changed", (ev: Event) => {
|
||||
element.addEventListener("card-visibility-changed", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._updateVisibility();
|
||||
});
|
||||
this._element.addEventListener(
|
||||
element.addEventListener(
|
||||
"ll-upgrade",
|
||||
(ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
if (this.hass) {
|
||||
this._element!.hass = this.hass;
|
||||
}
|
||||
fireEvent(this, "card-updated");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
this._element.addEventListener(
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._loadElement(config);
|
||||
this._buildElement(config);
|
||||
fireEvent(this, "card-updated");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
return element;
|
||||
}
|
||||
|
||||
private _buildElement(config: LovelaceCardConfig) {
|
||||
this._element = this._createElement(config);
|
||||
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<typeof this>): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this._element) {
|
||||
this.load();
|
||||
}
|
||||
}
|
||||
|
||||
protected update(changedProps: PropertyValues<typeof this>) {
|
||||
super.update(changedProps);
|
||||
|
||||
if (this._element) {
|
||||
if (changedProps.has("config")) {
|
||||
const elementConfig = this._elementConfig;
|
||||
if (this.config !== elementConfig && this.config) {
|
||||
const typeChanged = this.config?.type !== elementConfig?.type;
|
||||
if (typeChanged) {
|
||||
this._loadElement(this.config);
|
||||
} else {
|
||||
this._updateElement(this.config);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changedProps.has("hass")) {
|
||||
try {
|
||||
if (this.hass) {
|
||||
this._element.hass = this.hass;
|
||||
}
|
||||
this._element.hass = this.hass;
|
||||
} catch (e: any) {
|
||||
this._loadElement(createErrorCardConfig(e.message, null));
|
||||
this._buildElement(createErrorCardConfig(e.message, null));
|
||||
}
|
||||
}
|
||||
if (changedProps.has("preview")) {
|
||||
@@ -173,17 +147,18 @@ export class HuiCard extends ReactiveElement {
|
||||
// For backwards compatibility
|
||||
(this._element as any).editMode = this.preview;
|
||||
} catch (e: any) {
|
||||
this._loadElement(createErrorCardConfig(e.message, null));
|
||||
this._buildElement(createErrorCardConfig(e.message, null));
|
||||
}
|
||||
}
|
||||
if (changedProps.has("isPanel")) {
|
||||
this._element.isPanel = this.isPanel;
|
||||
}
|
||||
if (changedProps.has("layout")) {
|
||||
this._element.layout = this.layout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(
|
||||
changedProps: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||
): void {
|
||||
if (changedProps.has("hass") || changedProps.has("preview")) {
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
@@ -41,7 +41,6 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
element.load();
|
||||
return element;
|
||||
}
|
||||
|
||||
|
@@ -36,11 +36,7 @@ import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { createHeaderFooterElement } from "../create-element/create-header-footer-element";
|
||||
import {
|
||||
LovelaceCard,
|
||||
LovelaceHeaderFooter,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import { LovelaceCard, LovelaceHeaderFooter } from "../types";
|
||||
import { HuiErrorCard } from "./hui-error-card";
|
||||
import { EntityCardConfig } from "./types";
|
||||
|
||||
@@ -245,15 +241,6 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
|
||||
}
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
return {
|
||||
grid_columns: 2,
|
||||
grid_rows: 2,
|
||||
grid_min_columns: 2,
|
||||
grid_min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
iconColorCSS,
|
||||
|
@@ -249,7 +249,6 @@ export class HuiEntityFilterCard
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
element.load();
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
@@ -92,7 +92,6 @@ class HuiGridCard extends HuiStackCard<GridCardConfig> {
|
||||
}
|
||||
|
||||
:host([square]) #root > *:not([hidden]) {
|
||||
display: block;
|
||||
grid-row: 1 / 1;
|
||||
grid-column: 1 / 1;
|
||||
}
|
||||
|
@@ -30,10 +30,7 @@ export class HuiHorizontalStackCard extends HuiStackCard {
|
||||
height: 100%;
|
||||
gap: var(--horizontal-stack-card-gap, var(--stack-card-gap, 8px));
|
||||
}
|
||||
#root > hui-card {
|
||||
display: contents;
|
||||
}
|
||||
#root > hui-card > * {
|
||||
#root > * {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@@ -22,24 +21,11 @@ import { HomeAssistant } from "../../../types";
|
||||
import "../card-features/hui-card-features";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { HumidifierCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-humidifier-card")
|
||||
export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
private _resizeController = new ResizeController(this, {
|
||||
callback: (entries) => {
|
||||
const container = entries[0]?.target.shadowRoot?.querySelector(
|
||||
".container"
|
||||
) as HTMLElement | undefined;
|
||||
return container?.clientHeight;
|
||||
},
|
||||
});
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-humidifier-card-editor");
|
||||
return document.createElement("hui-humidifier-card-editor");
|
||||
@@ -137,25 +123,16 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const color = stateColorCss(stateObj);
|
||||
|
||||
const controlMaxWidth = this._resizeController.value
|
||||
? `${this._resizeController.value}px`
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<p class="title">${name}</p>
|
||||
<div class="container">
|
||||
<ha-state-control-humidifier-humidity
|
||||
style=${styleMap({
|
||||
maxWidth: controlMaxWidth,
|
||||
})}
|
||||
prevent-interaction-on-scroll
|
||||
.showCurrentAsPrimary=${this._config.show_current_as_primary}
|
||||
show-secondary
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-control-humidifier-humidity>
|
||||
</div>
|
||||
<ha-state-control-humidifier-humidity
|
||||
prevent-interaction-on-scroll
|
||||
.showCurrentAsPrimary=${this._config.show_current_as_primary}
|
||||
show-secondary
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-control-humidifier-humidity>
|
||||
<ha-icon-button
|
||||
class="more-info"
|
||||
.label=${this.hass!.localize(
|
||||
@@ -177,35 +154,12 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
const grid_columns = 4;
|
||||
let grid_rows = 5;
|
||||
let grid_min_rows = 2;
|
||||
const grid_min_columns = 2;
|
||||
if (this._config?.features?.length) {
|
||||
const featureHeight = Math.ceil((this._config.features.length * 2) / 3);
|
||||
grid_rows += featureHeight;
|
||||
grid_min_rows += featureHeight;
|
||||
}
|
||||
return {
|
||||
grid_columns,
|
||||
grid_rows,
|
||||
grid_min_rows,
|
||||
grid_min_columns,
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -224,28 +178,13 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
ha-state-control-humidifier-humidity {
|
||||
width: 100%;
|
||||
max-width: 344px; /* 12px + 12px + 320px */
|
||||
padding: 0 12px 12px 12px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.container:before {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
@@ -262,7 +201,6 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
|
||||
hui-card-features {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -7,11 +7,7 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { IFRAME_SANDBOX } from "../../../util/iframe";
|
||||
import {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { IframeCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-iframe-card")
|
||||
@@ -32,9 +28,6 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public isPanel = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public layout?: string;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() protected _config?: IframeCardConfig;
|
||||
@@ -63,16 +56,13 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
let padding = "";
|
||||
const ignoreAspectRatio = this.isPanel || this.layout === "grid";
|
||||
if (!ignoreAspectRatio) {
|
||||
if (this._config.aspect_ratio) {
|
||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
||||
padding = `${((100 * ratio.h) / ratio.w).toFixed(2)}%`;
|
||||
}
|
||||
} else {
|
||||
padding = "50%";
|
||||
if (!this.isPanel && this._config.aspect_ratio) {
|
||||
const ratio = parseAspectRatio(this._config.aspect_ratio);
|
||||
if (ratio && ratio.w > 0 && ratio.h > 0) {
|
||||
padding = `${((100 * ratio.h) / ratio.w).toFixed(2)}%`;
|
||||
}
|
||||
} else if (!this.isPanel) {
|
||||
padding = "50%";
|
||||
}
|
||||
|
||||
const target_protocol = new URL(this._config.url, location.toString())
|
||||
@@ -115,28 +105,26 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
return {
|
||||
grid_columns: 4,
|
||||
grid_rows: 4,
|
||||
grid_min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
:host([ispanel]) ha-card {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([ispanel]) #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
border: none;
|
||||
|
@@ -39,7 +39,7 @@ import { HomeAssistant } from "../../../types";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { processConfigEntities } from "../common/process-config-entities";
|
||||
import { EntityConfig } from "../entity-rows/types";
|
||||
import { LovelaceCard, LovelaceLayoutOptions } from "../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { MapCardConfig } from "./types";
|
||||
|
||||
export const DEFAULT_HOURS_TO_SHOW = 0;
|
||||
@@ -57,9 +57,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public isPanel = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public layout?: string;
|
||||
|
||||
@state() private _stateHistory?: HistoryStates;
|
||||
|
||||
@state()
|
||||
@@ -300,9 +297,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
|
||||
private _computePadding(): void {
|
||||
const root = this.shadowRoot!.getElementById("root");
|
||||
|
||||
const ignoreAspectRatio = this.isPanel || this.layout === "grid";
|
||||
if (!this._config || ignoreAspectRatio || !root) {
|
||||
if (!this._config || this.isPanel || !root) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -428,15 +423,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
);
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
return {
|
||||
grid_columns: 4,
|
||||
grid_rows: 4,
|
||||
grid_min_columns: 2,
|
||||
grid_min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
|
@@ -76,8 +76,6 @@ class HuiSensorCard extends HuiEntityCard {
|
||||
return {
|
||||
grid_columns: 2,
|
||||
grid_rows: 2,
|
||||
grid_min_columns: 2,
|
||||
grid_min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -56,7 +56,7 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
card.hass = this.hass;
|
||||
});
|
||||
}
|
||||
if (changedProperties.has("preview")) {
|
||||
if (changedProperties.has("editMode")) {
|
||||
this._cards.forEach((card) => {
|
||||
card.preview = this.preview;
|
||||
});
|
||||
@@ -69,7 +69,6 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
element.load();
|
||||
return element;
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -16,12 +16,12 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-state-icon";
|
||||
import {
|
||||
StatisticsMetaData,
|
||||
fetchStatistic,
|
||||
getDisplayUnit,
|
||||
getStatisticLabel,
|
||||
getStatisticMetadata,
|
||||
isExternalStatistic,
|
||||
StatisticsMetaData,
|
||||
} from "../../../data/recorder";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceHeaderFooter,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import { HuiErrorCard } from "./hui-error-card";
|
||||
import { EntityCardConfig, StatisticCardConfig } from "./types";
|
||||
@@ -255,15 +254,6 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
|
||||
fireEvent(this, "hass-more-info", { entityId: this._config!.entity });
|
||||
}
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
return {
|
||||
grid_columns: 2,
|
||||
grid_rows: 2,
|
||||
grid_min_columns: 2,
|
||||
grid_min_rows: 2,
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@@ -19,27 +18,14 @@ import "../../../components/ha-icon-button";
|
||||
import { ClimateEntity } from "../../../data/climate";
|
||||
import "../../../state-control/climate/ha-state-control-climate-temperature";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../card-features/hui-card-features";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import "../card-features/hui-card-features";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { ThermostatCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-thermostat-card")
|
||||
export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
private _resizeController = new ResizeController(this, {
|
||||
callback: (entries) => {
|
||||
const container = entries[0]?.target.shadowRoot?.querySelector(
|
||||
".container"
|
||||
) as HTMLElement | undefined;
|
||||
return container?.clientHeight;
|
||||
},
|
||||
});
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-thermostat-card-editor");
|
||||
return document.createElement("hui-thermostat-card-editor");
|
||||
@@ -129,25 +115,16 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const color = stateColorCss(stateObj);
|
||||
|
||||
const controlMaxWidth = this._resizeController.value
|
||||
? `${this._resizeController.value}px`
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<p class="title">${name}</p>
|
||||
<div class="container">
|
||||
<ha-state-control-climate-temperature
|
||||
style=${styleMap({
|
||||
maxWidth: controlMaxWidth,
|
||||
})}
|
||||
prevent-interaction-on-scroll
|
||||
.showCurrentAsPrimary=${this._config.show_current_as_primary}
|
||||
show-secondary
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-control-climate-temperature>
|
||||
</div>
|
||||
<ha-state-control-climate-temperature
|
||||
prevent-interaction-on-scroll
|
||||
.showCurrentAsPrimary=${this._config.show_current_as_primary}
|
||||
show-secondary
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-control-climate-temperature>
|
||||
<ha-icon-button
|
||||
class="more-info"
|
||||
.label=${this.hass!.localize(
|
||||
@@ -169,35 +146,12 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
const grid_columns = 4;
|
||||
let grid_rows = 5;
|
||||
let grid_min_rows = 2;
|
||||
const grid_min_columns = 2;
|
||||
if (this._config?.features?.length) {
|
||||
const featureHeight = Math.ceil((this._config.features.length * 2) / 3);
|
||||
grid_rows += featureHeight;
|
||||
grid_min_rows += featureHeight;
|
||||
}
|
||||
return {
|
||||
grid_columns,
|
||||
grid_rows,
|
||||
grid_min_rows,
|
||||
grid_min_columns,
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -216,28 +170,13 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
ha-state-control-climate-temperature {
|
||||
width: 100%;
|
||||
max-width: 344px; /* 12px + 12px + 320px */
|
||||
padding: 0 12px 12px 12px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.container:before {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
@@ -254,7 +193,6 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
|
||||
hui-card-features {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -122,21 +122,17 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
const grid_columns = 2;
|
||||
let grid_rows = 1;
|
||||
const options = {
|
||||
grid_columns: 2,
|
||||
grid_rows: 1,
|
||||
};
|
||||
if (this._config?.features?.length) {
|
||||
const featureHeight = Math.ceil((this._config.features.length * 2) / 3);
|
||||
grid_rows += featureHeight;
|
||||
options.grid_rows += Math.ceil((this._config.features.length * 2) / 3);
|
||||
}
|
||||
if (this._config?.vertical) {
|
||||
grid_rows!++;
|
||||
options.grid_rows++;
|
||||
}
|
||||
return {
|
||||
grid_columns,
|
||||
grid_rows,
|
||||
grid_min_rows: grid_rows,
|
||||
grid_min_columns: grid_columns,
|
||||
};
|
||||
return options;
|
||||
}
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
@@ -242,14 +238,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
></ha-relative-time>
|
||||
`;
|
||||
}
|
||||
if (content === "last-updated") {
|
||||
return html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_updated}
|
||||
></ha-relative-time>
|
||||
`;
|
||||
}
|
||||
if (content === "last_triggered") {
|
||||
return html`
|
||||
<ha-relative-time
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@@ -15,6 +14,7 @@ import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_elemen
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
@@ -38,11 +38,7 @@ import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { WeatherForecastCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-weather-forecast-card")
|
||||
@@ -78,21 +74,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _subscribed?: Promise<() => void>;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => {
|
||||
const width = entries[0]?.contentRect.width;
|
||||
if (width < 245) {
|
||||
return "very-very-narrow";
|
||||
}
|
||||
if (width < 300) {
|
||||
return "very-narrow";
|
||||
}
|
||||
if (width < 375) {
|
||||
return "narrow";
|
||||
}
|
||||
return "regular";
|
||||
},
|
||||
});
|
||||
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
||||
@property({ type: Boolean, reflect: true }) public veryVeryNarrow = false;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
private _needForecastSubscription() {
|
||||
return (
|
||||
@@ -133,10 +118,14 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
if (this.hasUpdated && this._config && this.hass) {
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
this.updateComplete.then(() => this._attachObserver());
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
this._unsubscribeForecastEvents();
|
||||
}
|
||||
|
||||
@@ -170,6 +159,16 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
}
|
||||
|
||||
public willUpdate(): void {
|
||||
if (!this.hasUpdated) {
|
||||
this._measureCard();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._attachObserver();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (!this._config || !this.hass) {
|
||||
@@ -227,10 +226,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
const forecast =
|
||||
this._config?.show_forecast !== false && forecastData?.forecast?.length
|
||||
? forecastData.forecast.slice(
|
||||
0,
|
||||
this._sizeController.value === "very-very-narrow" ? 3 : 5
|
||||
)
|
||||
? forecastData.forecast.slice(0, this.veryVeryNarrow ? 3 : 5)
|
||||
: undefined;
|
||||
const weather = !forecast || this._config?.show_current !== false;
|
||||
|
||||
@@ -242,7 +238,6 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
class=${ifDefined(this._sizeController.value)}
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(this._config!.hold_action),
|
||||
@@ -421,39 +416,52 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
private _showValue(item?: any): boolean {
|
||||
return typeof item !== "undefined" && item !== null;
|
||||
private async _attachObserver(): Promise<void> {
|
||||
if (!this._resizeObserver) {
|
||||
this._resizeObserver = new ResizeObserver(
|
||||
debounce(() => this._measureCard(), 250, false)
|
||||
);
|
||||
}
|
||||
const card = this.shadowRoot!.querySelector("ha-card");
|
||||
// If we show an error or warning there is no ha-card
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
this._resizeObserver.observe(card);
|
||||
}
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
if (
|
||||
this._config?.show_current !== false &&
|
||||
this._config?.show_forecast !== false
|
||||
) {
|
||||
return {
|
||||
grid_columns: 4,
|
||||
grid_min_columns: 2,
|
||||
grid_rows: 3,
|
||||
grid_min_rows: 3,
|
||||
};
|
||||
private _measureCard() {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
grid_columns: 4,
|
||||
grid_min_columns: 2,
|
||||
grid_rows: 2,
|
||||
grid_min_rows: 1,
|
||||
};
|
||||
|
||||
const card = this.shadowRoot!.querySelector("ha-card");
|
||||
// If we show an error or warning there is no ha-card
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (card.offsetWidth < 375) {
|
||||
this.setAttribute("narrow", "");
|
||||
} else {
|
||||
this.removeAttribute("narrow");
|
||||
}
|
||||
if (card.offsetWidth < 300) {
|
||||
this.setAttribute("verynarrow", "");
|
||||
} else {
|
||||
this.removeAttribute("verynarrow");
|
||||
}
|
||||
this.veryVeryNarrow = card.offsetWidth < 245;
|
||||
}
|
||||
|
||||
private _showValue(item?: any): boolean {
|
||||
return typeof item !== "undefined" && item !== null;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
weatherSVGStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
@@ -604,48 +612,48 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
/* ============= NARROW ============= */
|
||||
|
||||
[class*="narrow"] .icon-image {
|
||||
:host([narrow]) .icon-image {
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
[class*="narrow"] .weather-image {
|
||||
:host([narrow]) .weather-image {
|
||||
flex: 0 0 52px;
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
[class*="narrow"] .icon-image .weather-icon {
|
||||
:host([narrow]) .icon-image .weather-icon {
|
||||
--mdc-icon-size: 52px;
|
||||
}
|
||||
|
||||
[class*="narrow"] .state,
|
||||
[class*="narrow"] .temp-attribute .temp {
|
||||
:host([narrow]) .state,
|
||||
:host([narrow]) .temp-attribute .temp {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
[class*="narrow"] .temp-attribute .temp {
|
||||
:host([narrow]) .temp-attribute .temp {
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
|
||||
[class*="narrow"] .temp span {
|
||||
:host([narrow]) .temp span {
|
||||
top: 1px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* ============= VERY NARROW ============= */
|
||||
|
||||
[class*="very-narrow"] .name,
|
||||
[class*="very-narrow"] .attribute {
|
||||
:host([veryNarrow]) .name,
|
||||
:host([veryNarrow]) .attribute {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[class*="very-narrow"] .info {
|
||||
:host([veryNarrow]) .info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
[class*="very-narrow"] .name-state {
|
||||
:host([veryNarrow]) .name-state {
|
||||
padding-right: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: initial;
|
||||
@@ -653,18 +661,18 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
/* ============= VERY VERY NARROW ============= */
|
||||
|
||||
[class*="very-very-narrow"] .info {
|
||||
:host([veryVeryNarrow]) .info {
|
||||
padding-top: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[class*="very-very-narrow"] .content {
|
||||
:host([veryVeryNarrow]) .content {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[class*="very-very-narrow"] .icon-image {
|
||||
:host([veryVeryNarrow]) .icon-image {
|
||||
margin-right: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: initial;
|
||||
|
@@ -255,14 +255,15 @@ export class HaGridLayoutSlider extends LitElement {
|
||||
>
|
||||
<div id="slider" class="slider">
|
||||
<div class="track">
|
||||
<div class="background"></div>
|
||||
<div
|
||||
class="active"
|
||||
style=${styleMap({
|
||||
"--min": `${this.min / this._range}`,
|
||||
"--max": `${1 - this.max / this._range}`,
|
||||
})}
|
||||
></div>
|
||||
<div class="background">
|
||||
<div
|
||||
class="active"
|
||||
style=${styleMap({
|
||||
"--min": `${this.min / this._range}`,
|
||||
"--max": `${1 - this.max / this._range}`,
|
||||
})}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
${this.value !== undefined
|
||||
? html`<div class="handle"></div>`
|
||||
@@ -322,12 +323,11 @@ export class HaGridLayoutSlider extends LitElement {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--disabled-color);
|
||||
opacity: 0.2;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.active {
|
||||
position: absolute;
|
||||
background: grey;
|
||||
opacity: 0.7;
|
||||
top: 0;
|
||||
right: calc(var(--max) * 100%);
|
||||
bottom: 0;
|
||||
@@ -375,9 +375,6 @@ export class HaGridLayoutSlider extends LitElement {
|
||||
:host(:disabled) .slider {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
:host(:disabled) .handle:after {
|
||||
background: var(--disabled-color);
|
||||
}
|
||||
.pressed .handle {
|
||||
transition: none;
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { HuiCard } from "../../cards/hui-card";
|
||||
import { computeSizeOnGrid } from "../../sections/hui-grid-section";
|
||||
import { DEFAULT_GRID_OPTIONS } from "../../sections/hui-grid-section";
|
||||
import { LovelaceLayoutOptions } from "../../types";
|
||||
|
||||
@customElement("hui-card-layout-editor")
|
||||
@@ -38,29 +38,28 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
|
||||
private _cardElement?: HuiCard;
|
||||
|
||||
private _mergedOptions = memoizeOne(
|
||||
private _gridSizeValue = memoizeOne(
|
||||
(
|
||||
options?: LovelaceLayoutOptions,
|
||||
defaultOptions?: LovelaceLayoutOptions
|
||||
) => ({
|
||||
...defaultOptions,
|
||||
...options,
|
||||
rows:
|
||||
options?.grid_rows ??
|
||||
defaultOptions?.grid_rows ??
|
||||
DEFAULT_GRID_OPTIONS.grid_rows,
|
||||
columns:
|
||||
options?.grid_columns ??
|
||||
defaultOptions?.grid_columns ??
|
||||
DEFAULT_GRID_OPTIONS.grid_columns,
|
||||
})
|
||||
);
|
||||
|
||||
private _gridSizeValue = memoizeOne(computeSizeOnGrid);
|
||||
|
||||
private _isDefault = memoizeOne(
|
||||
(options?: LovelaceLayoutOptions) =>
|
||||
options?.grid_columns === undefined && options?.grid_rows === undefined
|
||||
);
|
||||
|
||||
render() {
|
||||
const options = this._mergedOptions(
|
||||
this.config.layout_options,
|
||||
this._defaultLayoutOptions
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<p class="intro">
|
||||
@@ -124,13 +123,12 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
: html`
|
||||
<ha-grid-size-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._gridSizeValue(options)}
|
||||
.value=${this._gridSizeValue(
|
||||
this.config.layout_options,
|
||||
this._defaultLayoutOptions
|
||||
)}
|
||||
.isDefault=${this._isDefault(this.config.layout_options)}
|
||||
@value-changed=${this._gridSizeChanged}
|
||||
.rowMin=${options.grid_min_rows}
|
||||
.rowMax=${options.grid_max_rows}
|
||||
.columnMin=${options.grid_min_columns}
|
||||
.columnMax=${options.grid_max_columns}
|
||||
></ha-grid-size-picker>
|
||||
`}
|
||||
`;
|
||||
@@ -148,7 +146,6 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
this._defaultLayoutOptions =
|
||||
this._cardElement?.getElementLayoutOptions();
|
||||
});
|
||||
this._cardElement.load();
|
||||
this._defaultLayoutOptions = this._cardElement.getElementLayoutOptions();
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@@ -16,7 +16,6 @@ import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { stripDiacritics } from "../../../../common/string/strip-diacritics";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import "../../../../components/search-input";
|
||||
import { isUnavailableState } from "../../../../data/entity";
|
||||
@@ -29,7 +28,6 @@ import {
|
||||
getCustomCardEntry,
|
||||
} from "../../../../data/lovelace_custom_cards";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getStripDiacriticsFn } from "../../../../util/fuse";
|
||||
import {
|
||||
calcUnusedEntities,
|
||||
computeUsedEntities,
|
||||
@@ -88,10 +86,9 @@ export class HuiCardPicker extends LitElement {
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
getFn: getStripDiacriticsFn,
|
||||
};
|
||||
const fuse = new Fuse(cards, options);
|
||||
cards = fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
||||
cards = fuse.search(filter).map((result) => result.item);
|
||||
return cardElements.filter((cardElement: CardElement) =>
|
||||
cards.includes(cardElement.card)
|
||||
);
|
||||
|
@@ -201,12 +201,6 @@ export class HuiTileCardEditor
|
||||
),
|
||||
value: "last-changed",
|
||||
},
|
||||
{
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.tile.state_content_options.last-updated`
|
||||
),
|
||||
value: "last-updated",
|
||||
},
|
||||
...Object.keys(stateObj?.attributes ?? {})
|
||||
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
|
||||
.map((attribute) => ({
|
||||
|
@@ -15,7 +15,6 @@ import { HuiCard } from "../cards/hui-card";
|
||||
import "../components/hui-card-edit-mode";
|
||||
import { moveCard } from "../editor/config-util";
|
||||
import type { Lovelace, LovelaceLayoutOptions } from "../types";
|
||||
import { conditionalClamp } from "../../../common/number/clamp";
|
||||
|
||||
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
delay: 100,
|
||||
@@ -24,41 +23,9 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
invertedSwapThreshold: 0.7,
|
||||
} as HaSortableOptions;
|
||||
|
||||
export const DEFAULT_GRID_OPTIONS = {
|
||||
export const DEFAULT_GRID_OPTIONS: LovelaceLayoutOptions = {
|
||||
grid_columns: 4,
|
||||
grid_rows: 1,
|
||||
} as const satisfies LovelaceLayoutOptions;
|
||||
|
||||
type GridSizeValue = {
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
};
|
||||
|
||||
export const computeSizeOnGrid = (
|
||||
options: LovelaceLayoutOptions
|
||||
): GridSizeValue => {
|
||||
const rows =
|
||||
typeof options.grid_rows === "number"
|
||||
? conditionalClamp(
|
||||
options.grid_rows,
|
||||
options.grid_min_rows,
|
||||
options.grid_max_rows
|
||||
)
|
||||
: DEFAULT_GRID_OPTIONS.grid_rows;
|
||||
|
||||
const columns =
|
||||
typeof options.grid_columns === "number"
|
||||
? conditionalClamp(
|
||||
options.grid_columns,
|
||||
options.grid_min_columns,
|
||||
options.grid_max_columns
|
||||
)
|
||||
: DEFAULT_GRID_OPTIONS.grid_columns;
|
||||
|
||||
return {
|
||||
rows,
|
||||
columns,
|
||||
};
|
||||
};
|
||||
|
||||
export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
@@ -131,16 +98,17 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
(cardConfig) => this._getKey(cardConfig),
|
||||
(_cardConfig, idx) => {
|
||||
const card = this.cards![idx];
|
||||
card.layout = "grid";
|
||||
const layoutOptions = card.getLayoutOptions();
|
||||
|
||||
const { rows, columns } = computeSizeOnGrid(layoutOptions);
|
||||
|
||||
const columnSize =
|
||||
layoutOptions.grid_columns ?? DEFAULT_GRID_OPTIONS.grid_columns;
|
||||
const rowSize =
|
||||
layoutOptions.grid_rows ?? DEFAULT_GRID_OPTIONS.grid_rows;
|
||||
return html`
|
||||
<div
|
||||
style=${styleMap({
|
||||
"--column-size": columns,
|
||||
"--row-size": rows,
|
||||
"--column-size": columnSize,
|
||||
"--row-size": rowSize,
|
||||
})}
|
||||
class="card ${classMap({
|
||||
"fit-rows": typeof layoutOptions?.grid_rows === "number",
|
||||
|
@@ -64,7 +64,6 @@ export class HuiSection extends ReactiveElement {
|
||||
ev.stopPropagation();
|
||||
this._cards = [...this._cards];
|
||||
});
|
||||
element.load();
|
||||
return element;
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user