Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
ebe5207b6e Improve datatable 2024-06-25 17:05:51 +02:00
108 changed files with 1532 additions and 3007 deletions

View File

@@ -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": {

View File

@@ -32,7 +32,4 @@ module.exports = {
}
return version[1];
},
isDevContainer() {
return process.env.DEV_CONTAINER === "1";
},
};

View File

@@ -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("-");

View File

@@ -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,

View File

@@ -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: [

View File

@@ -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>

View File

@@ -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") %>

View File

@@ -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>

View File

@@ -1,3 +1,4 @@
import "../../../src/resources/safari-14-attachshadow-patch";
import "./layout/hc-connect";
import("../../../src/resources/ha-style");

View File

@@ -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",

View File

@@ -1,4 +1,4 @@
import "./util/is_frontpage";
import "../../src/resources/safari-14-attachshadow-patch";
import "./ha-demo";
import("../../src/resources/ha-style");

View File

@@ -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>

View File

@@ -1 +0,0 @@
export const isFrontpageEmbed = document.location.search === "?frontpage";

View File

@@ -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);
}

View File

@@ -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");

View File

@@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -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

View File

@@ -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"

View File

@@ -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
);

View File

@@ -1,2 +0,0 @@
export const stripDiacritics = (str: string) =>
str.normalize("NFD").replace(/[\u0300-\u036F]/g, "");

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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,
});
};

View File

@@ -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;
}
}

View File

@@ -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 = {

View File

@@ -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 },
});
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
`;
}
}

View File

@@ -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");

View File

@@ -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,
});

View File

@@ -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;
};

View File

@@ -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;
}
}

View File

@@ -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(

View File

@@ -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__;

View File

@@ -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";

View File

@@ -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(

View File

@@ -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,

View File

@@ -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)

View File

@@ -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;
}
}

View File

@@ -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);
}
}
`;
}

View File

@@ -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>
`;

View File

@@ -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);
}
`;
}
}

View File

@@ -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);
}
);

View File

@@ -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">

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;
}
`,
];
}

View File

@@ -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}

View File

@@ -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;
}
`,
];
}

View File

@@ -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;
}

View File

@@ -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>`,
}),

View File

@@ -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`

View File

@@ -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,

View File

@@ -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,

View File

@@ -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),
];
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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,

View File

@@ -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"

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;
}
`,
];
}

View File

@@ -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}

View File

@@ -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;
}
`,
];

View File

@@ -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

View File

@@ -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;
}
`,
];

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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 = [];
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -249,7 +249,6 @@ export class HuiEntityFilterCard
element.hass = this.hass;
element.preview = this.preview;
element.config = cardConfig;
element.load();
return element;
}
}

View File

@@ -92,7 +92,6 @@ class HuiGridCard extends HuiStackCard<GridCardConfig> {
}
:host([square]) #root > *:not([hidden]) {
display: block;
grid-row: 1 / 1;
grid-column: 1 / 1;
}

View File

@@ -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;
}

View File

@@ -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;
}
`;
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -76,8 +76,6 @@ class HuiSensorCard extends HuiEntityCard {
return {
grid_columns: 2,
grid_rows: 2,
grid_min_columns: 2,
grid_min_rows: 2,
};
}

View File

@@ -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;
}

View File

@@ -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`

View File

@@ -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;
}
`;
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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)
);

View File

@@ -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) => ({

View File

@@ -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",

View File

@@ -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