Compare commits

..

1 Commits

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

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

@@ -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,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,5 +1,4 @@
import "../../src/resources/safari-14-attachshadow-patch";
import "./util/is_frontpage";
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

@@ -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 = "20240702.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,280 +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,
this._hiddenColumns
);
if (!this._columnOrder) {
this._columnOrder = columns.map((col) => col.key);
} else {
columns.forEach((col) => {
if (!this._columnOrder!.includes(col.key)) {
this._columnOrder!.push(col.key);
if (col.defaultHidden) {
hidden.push(col.key);
}
}
});
}
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

@@ -394,7 +394,6 @@ class GroupEntity extends Entity {
}
const TYPES = {
automation: ToggleEntity,
alarm_control_panel: AlarmControlPanelEntity,
climate: ClimateEntity,
cover: CoverEntity,

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[];
@@ -412,17 +405,13 @@ export class HuiAreaCard
if (this._config.show_camera && "camera" in entitiesByDomain) {
cameraEntityId = entitiesByDomain.camera[0].entity_id;
}
cameraEntityId = "camera.demo_camera";
const imageClass = area.picture || cameraEntityId;
const ignoreAspectRatio = imageClass || this.layout === "grid";
return html`
<ha-card
class=${imageClass ? "image" : ""}
style=${styleMap({
paddingBottom: ignoreAspectRatio
paddingBottom: imageClass
? "0"
: `${((100 * this._ratio!.h) / this._ratio!.w).toFixed(2)}%`,
})}
@@ -545,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 {

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

@@ -40,11 +40,7 @@ import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-marquee";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type {
LovelaceCard,
LovelaceCardEditor,
LovelaceLayoutOptions,
} from "../types";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import { MediaControlCardConfig } from "./types";
@customElement("hui-media-control-card")
@@ -586,15 +582,6 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
}
}
public getLayoutOptions(): LovelaceLayoutOptions {
return {
grid_columns: 4,
grid_min_columns: 2,
grid_rows: 3,
grid_min_rows: 3,
};
}
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;
}

View File

@@ -43,17 +43,12 @@ export interface LovelaceBadge extends HTMLElement {
export type LovelaceLayoutOptions = {
grid_columns?: number;
grid_rows?: number;
grid_max_columns?: number;
grid_min_columns?: number;
grid_min_rows?: number;
grid_max_rows?: number;
};
export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant;
isPanel?: boolean;
preview?: boolean;
layout?: string;
getCardSize(): number | Promise<number>;
getLayoutOptions?(): LovelaceLayoutOptions;
setConfig(config: LovelaceCardConfig): void;

View File

@@ -82,7 +82,6 @@ export class HUIView extends ReactiveElement {
ev.stopPropagation();
this._cards = [...this._cards];
});
element.load();
return element;
}

View File

@@ -419,6 +419,11 @@
"manual": "Manual Entry"
}
},
"image": {
"select_image": "Select image",
"upload": "Upload picture",
"url": "Local path or web URL"
},
"text": {
"show_password": "Show password",
"hide_password": "Hide password"
@@ -530,8 +535,7 @@
"selected": "Selected {selected}",
"close_select_mode": "Close selection mode",
"select_all": "Select all",
"select_none": "Select none",
"settings": "Customize table"
"select_none": "Select none"
},
"config-entry-picker": {
"config_entry": "Integration"
@@ -800,14 +804,7 @@
"filtering_by": "Filtering by",
"hidden": "{number} hidden",
"clear": "Clear",
"ungrouped": "Ungrouped",
"settings": {
"header": "Customize",
"hide": "Hide column {title}",
"show": "Show column {title}",
"done": "Done",
"restore": "Restore defaults"
}
"ungrouped": "Ungrouped"
},
"media-browser": {
"tts": {
@@ -2079,7 +2076,6 @@
"download_backup": "[%key:supervisor::backup::download_backup%]",
"remove_backup": "[%key:supervisor::backup::delete_backup_title%]",
"name": "[%key:supervisor::backup::name%]",
"path": "Path",
"size": "[%key:supervisor::backup::size%]",
"created": "[%key:supervisor::backup::created%]",
"no_backups": "[%key:supervisor::backup::no_backups%]",
@@ -2674,7 +2670,6 @@
"caption": "Expose",
"headers": {
"name": "Name",
"entity_id": "Entity ID",
"area": "Area",
"domain": "Domain",
"assistants": "Assistants",
@@ -2733,8 +2728,7 @@
"actions": "Actions",
"state": "State",
"category": "Category",
"area": "Area",
"icon": "Icon"
"area": "Area"
},
"bulk_action": "Action",
"bulk_actions": {
@@ -2770,8 +2764,6 @@
"unavailable": "Automation is unavailable",
"migrate": "Migrate",
"duplicate": "[%key:ui::common::duplicate%]",
"take_control": "Take control",
"confirm_take_control": "Your are viewing a preview of the automation config, do you want to take control?",
"run": "[%key:ui::panel::config::automation::editor::actions::run%]",
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
"show_trace": "Traces",
@@ -3605,8 +3597,7 @@
"name": "Name",
"state": "State",
"category": "Category",
"area": "Area",
"icon": "Icon"
"area": "Area"
},
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
@@ -3643,8 +3634,6 @@
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
"change_mode": "[%key:ui::panel::config::automation::editor::change_mode%]",
"take_control": "[%key:ui::panel::config::automation::editor::take_control%]",
"confirm_take_control": "Your are viewing a preview of the script config, do you want to take control?",
"read_only": "This script cannot be edited from the UI, because it is not stored in the ''scripts.yaml'' file.",
"unavailable": "Script is unavailable",
"migrate": "Migrate",
@@ -3721,8 +3710,7 @@
"name": "Name",
"last_activated": "Last activated",
"category": "Category",
"area": "Area",
"icon": "Icon"
"area": "Area"
},
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
@@ -4045,7 +4033,6 @@
"update_device_error": "Updating the device failed",
"disabled": "Disabled",
"data_table": {
"icon": "Icon",
"device": "Device",
"manufacturer": "Manufacturer",
"model": "Model",
@@ -4156,18 +4143,10 @@
"delete": "Delete",
"create": "Create",
"update": "Update",
"confirm_delete_user_title": "Delete user account",
"confirm_delete_user_text": "The user account for ''{name}'' will be permanently deleted. You can still track the user, but the person will no longer be able to login.",
"allow_login": "Allow login",
"allow_login_description": "Allow access using username and password.",
"username": "[%key:ui::panel::config::users::editor::username%]",
"password": "[%key:ui::panel::config::users::editor::password%]",
"confirm_delete_user": "Are you sure you want to delete the user account for {name}? You can still track the user, but the person will no longer be able to login.",
"admin": "[%key:ui::panel::config::users::editor::admin%]",
"admin_description": "[%key:ui::panel::config::users::editor::admin_description%]",
"local_access_only": "[%key:ui::panel::config::users::editor::local_access_only%]",
"local_access_only_description": "[%key:ui::panel::config::users::editor::local_access_only_description%]",
"change_username": "[%key:ui::panel::config::users::editor::change_username%]",
"change_password": "[%key:ui::panel::config::users::editor::change_password%]"
"local_only": "[%key:ui::panel::config::users::editor::local_only%]",
"allow_login": "Allow person to login"
}
},
"zone": {
@@ -4274,9 +4253,9 @@
"config_entry": {
"application_credentials": {
"delete_title": "Application credentials",
"delete_prompt": "Would you like to also delete Application Credentials for this integration?",
"delete_detail": "If you delete them, you will need to enter credentials when setting up the integration again. If you keep them, they will be used automatically when setting up the integration again or may be acccessed from the Application Credentials menu.",
"delete_error_title": "Deleting application credentials failed",
"delete_prompt": "Would you like to also remove Application Credentials for this integration?",
"delete_detail": "If you remove them, you will need to enter credentials when setting up the integration again. If you keep them, they will be used automatically when setting up the integration again or may be acccessed from the Application Credentials menu.",
"delete_error_title": "Removing application credentials failed",
"dismiss": "Keep",
"learn_more": "Learn more about application credentials"
},
@@ -4407,7 +4386,6 @@
"caption": "View user",
"name": "Display name",
"username": "Username",
"password": "Password",
"change_password": "Change password",
"change_username": "Change username",
"activate_user": "Activate user",
@@ -4417,17 +4395,16 @@
"id": "ID",
"owner": "Owner",
"admin": "Administrator",
"admin_description": "Administrators can manage users, devices, automations and dashboards.",
"group": "Group",
"active": "Active",
"active_description": "Controls if user can login",
"local_access_only": "Local access only",
"local_access_only_description": "Can only log in from the local network",
"local_only": "Can only log in from the local network",
"system_generated": "System user",
"system_generated_read_only_users": "System users can not be updated.",
"system_generated_users_not_removable": "Unable to remove system users.",
"system_generated_users_not_editable": "Unable to update system users.",
"unnamed_user": "Unnamed User",
"confirm_user_deletion_title": "Delete {name}?",
"confirm_user_deletion_text": "This user will be permanently deleted."
"confirm_user_deletion_text": "This user will be permanently deleted.",
"active_tooltip": "Controls if user can login"
},
"add_user": {
"caption": "Add user",
@@ -4477,15 +4454,11 @@
"client_id": "OAuth client ID",
"application": "Integration"
},
"remove": {
"button": "Delete application credential",
"confirm_title": "Delete application credential?"
},
"remove_selected": {
"button": "Delete selected",
"confirm_title": "Do you want to delete {number} {number, plural,\n one {credential}\n other {credentials}\n}?",
"confirm_text": "Application credentials in use by an integration may not be deleted.",
"error_title": "Deleting application credentials failed"
"button": "Remove selected",
"confirm_title": "Do you want to remove {number} {number, plural,\n one {credential}\n other {credentials}\n}?",
"confirm_text": "Application credentials in use by an integration may not be removed.",
"error_title": "Removing application credentials failed"
},
"selected": "{number} selected"
}
@@ -5088,11 +5061,6 @@
"tips": {
"tip": "Tip!",
"join": "Join the community on our {forums}, {twitter}, {discord}, {blog} or {newsletter}",
"join_x": "X (formerly Twitter)",
"join_forums": "Forums",
"join_chat": "Chat",
"join_blog": "Blog",
"join_newsletter": "Newsletter",
"media_storage": "You can add network storage to your Home Assistant instance in the {storage} panel."
},
"analytics": {
@@ -5982,8 +5950,7 @@
"state_content": "State content",
"state_content_options": {
"state": "State",
"last-changed": "Last changed",
"last-updated": "Last updated"
"last-changed": "Last changed"
}
},
"vertical-stack": {
@@ -6897,7 +6864,7 @@
"forums": "Home Assistant forums",
"open_home_newsletter": "Building the Open Home newsletter",
"discord": "Discord chat",
"x": "[%key:ui::panel::config::tips::join_x%]",
"twitter": "Twitter",
"playstore": "Get it on Google Play",
"appstore": "Download on the App Store"
},

View File

@@ -70,18 +70,17 @@ export class AudioRecorder {
}
private async _createContext() {
// @ts-expect-error webkitAudioContext is not recognized
const context = new (AudioContext || webkitAudioContext)();
// @ts-ignore-next-line
this._context = new (window.AudioContext || window.webkitAudioContext)();
this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Syntax here must match an item of `parser.worker` in Webpack config in
// order for module to be parsed and a chunk to be properly created.
await context.audioWorklet.addModule(
/* webpackChunkName: "recorder-worklet" */
new URL("./recorder-worklet.js", import.meta.url)
await this._context.audioWorklet.addModule(
new URL("./recorder.worklet.js", import.meta.url)
);
this._context = context;
this._source = this._context.createMediaStreamSource(this._stream);
this._recorder = new AudioWorkletNode(this._context, "recorder-worklet");
this._recorder = new AudioWorkletNode(this._context, "recorder.worklet");
this._recorder.port.onmessage = (e) => {
if (!this._active) {
return;

View File

@@ -1,12 +0,0 @@
import Fuse from "fuse.js";
import { stripDiacritics } from "../common/string/strip-diacritics";
type GetFn = typeof Fuse.config.getFn;
export const getStripDiacriticsFn: GetFn = (obj, path) => {
const value = Fuse.config.getFn(obj, path);
if (Array.isArray(value)) {
return value.map((v) => stripDiacritics(v ?? ""));
}
return stripDiacritics((value as string | undefined) ?? "");
};

View File

@@ -18,4 +18,4 @@ class RecorderProcessor extends AudioWorkletProcessor {
}
}
registerProcessor("recorder-worklet", RecorderProcessor);
registerProcessor("recorder.worklet", RecorderProcessor);

221
yarn.lock
View File

@@ -1460,12 +1460,12 @@ __metadata:
languageName: node
linkType: hard
"@bundle-stats/plugin-webpack-filter@npm:4.13.3":
version: 4.13.3
resolution: "@bundle-stats/plugin-webpack-filter@npm:4.13.3"
"@bundle-stats/plugin-webpack-filter@npm:4.13.2":
version: 4.13.2
resolution: "@bundle-stats/plugin-webpack-filter@npm:4.13.2"
peerDependencies:
core-js: ^3.0.0
checksum: 10/5de079362fe592d29a32598646164aeea329e81697b51356d2f760932bf95f0ca2e5ac8c45324e2850f8450c12e8a8ec95d851fd15ab25522832ebd3e64aacfe
checksum: 10/92c2e3c05998762613e3fbed465bceb133c94b4182db502c42e6cbfc2016867101f72adcecb5d1791f73ebcb44039e0c8046737dfb1f49bbad7b1b617599184c
languageName: node
linkType: hard
@@ -3235,13 +3235,13 @@ __metadata:
languageName: node
linkType: hard
"@material/web@npm:1.5.1":
version: 1.5.1
resolution: "@material/web@npm:1.5.1"
"@material/web@npm:1.5.0":
version: 1.5.0
resolution: "@material/web@npm:1.5.0"
dependencies:
lit: "npm:^2.7.4 || ^3.0.0"
tslib: "npm:^2.4.0"
checksum: 10/9be6019068fbc4ed6873837ad549fd672c24beaacd9123bc9f3d72b7dfd67a1acdafab43bd484b50d40300e27e3742314915f4d6e6723b38be223b4547ae206f
checksum: 10/6bf651e8eaf33332b7f83aa04d473f6844a1f8280d5a2025a30583fbe03aa718de348260c5b9466d587f166772759aba0d100137e3e13d4d7c6fba6ffb79efa4
languageName: node
linkType: hard
@@ -3974,15 +3974,6 @@ __metadata:
languageName: node
linkType: hard
"@thepassle/axobject-query@npm:^4.0.0":
version: 4.0.0
resolution: "@thepassle/axobject-query@npm:4.0.0"
dependencies:
dequal: "npm:^2.0.3"
checksum: 10/919cb6ed90259cd0398b7e485dfbacae42423ff4202d5753c6545d3dfa9dc3d63e7f34941d6b94608c2730ec1539d30805411d9501c86951966e0d4aa0c4ae44
languageName: node
linkType: hard
"@thomasloven/round-slider@npm:0.6.0":
version: 0.6.0
resolution: "@thomasloven/round-slider@npm:0.6.0"
@@ -4045,10 +4036,10 @@ __metadata:
languageName: node
linkType: hard
"@types/chromecast-caf-receiver@npm:6.0.16":
version: 6.0.16
resolution: "@types/chromecast-caf-receiver@npm:6.0.16"
checksum: 10/8d60a8fb0a7c4c90d8d8bb7fd55007ef0b2367f0a2129b83895bd857dfdc21296934cb57829248806093d92f66a2fc05475c616b7656ebc27c9498011e2f1f01
"@types/chromecast-caf-receiver@npm:6.0.15":
version: 6.0.15
resolution: "@types/chromecast-caf-receiver@npm:6.0.15"
checksum: 10/532c926d01b8173013c0aa96fad3b4e3e8b8f02c993b52cbc654b4263c7e396cc7a1497e00561428cccdbe6bb2014824577e7fdf44f1ebd63e68a3815500fd86
languageName: node
linkType: hard
@@ -4365,10 +4356,10 @@ __metadata:
languageName: node
linkType: hard
"@types/mocha@npm:10.0.7":
version: 10.0.7
resolution: "@types/mocha@npm:10.0.7"
checksum: 10/4494871e8a867633d818b00d6f29d47379f9e23655b89ca728166ff2f0a406b97d376fcc3e7a570a3840f72abb03c886c5e66f50ae0f018376e4dc10ed179564
"@types/mocha@npm:10.0.6":
version: 10.0.6
resolution: "@types/mocha@npm:10.0.6"
checksum: 10/fc73626e81e89c32d06b7ff9b72c4177b46d579cdd932f796614adc026852d84cb849d743473ba572cb4d9ea6d8c04e3749552d326c26495ec1c4b46e6e0a0c0
languageName: node
linkType: hard
@@ -4584,15 +4575,15 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:7.14.1":
version: 7.14.1
resolution: "@typescript-eslint/eslint-plugin@npm:7.14.1"
"@typescript-eslint/eslint-plugin@npm:7.13.1":
version: 7.13.1
resolution: "@typescript-eslint/eslint-plugin@npm:7.13.1"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:7.14.1"
"@typescript-eslint/type-utils": "npm:7.14.1"
"@typescript-eslint/utils": "npm:7.14.1"
"@typescript-eslint/visitor-keys": "npm:7.14.1"
"@typescript-eslint/scope-manager": "npm:7.13.1"
"@typescript-eslint/type-utils": "npm:7.13.1"
"@typescript-eslint/utils": "npm:7.13.1"
"@typescript-eslint/visitor-keys": "npm:7.13.1"
graphemer: "npm:^1.4.0"
ignore: "npm:^5.3.1"
natural-compare: "npm:^1.4.0"
@@ -4603,44 +4594,44 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 10/48c815dbb92399965483c93b27816fad576c3b3227b59eebfe5525e24d07b39ec8b0c7459de83865c8d61c818696519f50b229714dd3ed705d5b35973bfcc781
checksum: 10/37fff8c302f93f5f88fc8d6e6c9151a7d1873a3c8af6e15547d737bdc066a6b8887fa54bcd8eb4e4ca6a11494051801c8e957eea8d8b4d4b078a477df6f10692
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:7.14.1":
version: 7.14.1
resolution: "@typescript-eslint/parser@npm:7.14.1"
"@typescript-eslint/parser@npm:7.13.1":
version: 7.13.1
resolution: "@typescript-eslint/parser@npm:7.13.1"
dependencies:
"@typescript-eslint/scope-manager": "npm:7.14.1"
"@typescript-eslint/types": "npm:7.14.1"
"@typescript-eslint/typescript-estree": "npm:7.14.1"
"@typescript-eslint/visitor-keys": "npm:7.14.1"
"@typescript-eslint/scope-manager": "npm:7.13.1"
"@typescript-eslint/types": "npm:7.13.1"
"@typescript-eslint/typescript-estree": "npm:7.13.1"
"@typescript-eslint/visitor-keys": "npm:7.13.1"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.56.0
peerDependenciesMeta:
typescript:
optional: true
checksum: 10/f521462a7005cab5e4923937dcf36713d9438ded175b53332ae469d91cc9eb18cb3a23768b3c52063464280baae83f6b66db28cebb2e262d6d869d1a898b23f3
checksum: 10/a76cfcf97c289110403b50a377e925f29cda74340de0526f68b0c34199ce643d9c31803e492217e0f3df28361d3019ced4806f974ea70529c559b26b70cec7ef
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:7.14.1":
version: 7.14.1
resolution: "@typescript-eslint/scope-manager@npm:7.14.1"
"@typescript-eslint/scope-manager@npm:7.13.1":
version: 7.13.1
resolution: "@typescript-eslint/scope-manager@npm:7.13.1"
dependencies:
"@typescript-eslint/types": "npm:7.14.1"
"@typescript-eslint/visitor-keys": "npm:7.14.1"
checksum: 10/600a7beb96f5b96f675125285137339c2438b5b26db203a66eef52dd409e8c0db0dafb22c94547dfb963f8efdf63b0fb59e05655e2dcf84d54624863365a59e7
"@typescript-eslint/types": "npm:7.13.1"
"@typescript-eslint/visitor-keys": "npm:7.13.1"
checksum: 10/fea9ab8f72ace1dd55d835037efe038c70021275581855820cdb7fc4b01e8afb51723856537adff1fdb0ea3899c1f8b593fd75c34b5087ca2ef2f7c72e610050
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:7.14.1":
version: 7.14.1
resolution: "@typescript-eslint/type-utils@npm:7.14.1"
"@typescript-eslint/type-utils@npm:7.13.1":
version: 7.13.1
resolution: "@typescript-eslint/type-utils@npm:7.13.1"
dependencies:
"@typescript-eslint/typescript-estree": "npm:7.14.1"
"@typescript-eslint/utils": "npm:7.14.1"
"@typescript-eslint/typescript-estree": "npm:7.13.1"
"@typescript-eslint/utils": "npm:7.13.1"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^1.3.0"
peerDependencies:
@@ -4648,23 +4639,23 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 10/75c279948a7e7e546d692e85a0b48fc3b648ffee1773feb7ff199aba1b0847a9a16c432b133aa72d26e645627403852b7dd24829f9b3badd6d4711c4cc38e9e4
checksum: 10/cc03cd44e125933511ea657e386c5cf427eb6a386fdb110cba0858598195fb4f8c71173b00b187f388a6713e16b919a2037a86e0be10f4c40c18bcbdbe06d5de
languageName: node
linkType: hard
"@typescript-eslint/types@npm:7.14.1":
version: 7.14.1
resolution: "@typescript-eslint/types@npm:7.14.1"
checksum: 10/608057582bb195bd746a7bfb7c04dac4be1d4602b8fa681b2d1d50b564362b681dc2ca293b13cc4c7acc454f3a09f1ea2580415347efb7853e5df8ba34b7acdb
"@typescript-eslint/types@npm:7.13.1":
version: 7.13.1
resolution: "@typescript-eslint/types@npm:7.13.1"
checksum: 10/006a5518608184c1d017b27fb4f66ce28bc75f89e2380ac42969ebdf0dc726af1cfcdf4ba36ce2858e9f6907d6f4295d3453859d7e9a35bc7855d4ebc900955d
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:7.14.1":
version: 7.14.1
resolution: "@typescript-eslint/typescript-estree@npm:7.14.1"
"@typescript-eslint/typescript-estree@npm:7.13.1":
version: 7.13.1
resolution: "@typescript-eslint/typescript-estree@npm:7.13.1"
dependencies:
"@typescript-eslint/types": "npm:7.14.1"
"@typescript-eslint/visitor-keys": "npm:7.14.1"
"@typescript-eslint/types": "npm:7.13.1"
"@typescript-eslint/visitor-keys": "npm:7.13.1"
debug: "npm:^4.3.4"
globby: "npm:^11.1.0"
is-glob: "npm:^4.0.3"
@@ -4674,31 +4665,31 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
checksum: 10/f75b956f7981712d3f85498f9d9fcc2243d79d6fe71b24bc688a7c43d2a4248f73ecfb78f9d58501fde87fc44b02e26c46f9ea2ae51eb8450db79ca169f91ef9
checksum: 10/5c68b5faa962e5f984067aa91770486af817858d2fa35b54a44fa4d5c0c612ba23b52b191d8051d9e4439e5425251e32861c81239e9400a29de057f8360537fb
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:7.14.1":
version: 7.14.1
resolution: "@typescript-eslint/utils@npm:7.14.1"
"@typescript-eslint/utils@npm:7.13.1":
version: 7.13.1
resolution: "@typescript-eslint/utils@npm:7.13.1"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.4.0"
"@typescript-eslint/scope-manager": "npm:7.14.1"
"@typescript-eslint/types": "npm:7.14.1"
"@typescript-eslint/typescript-estree": "npm:7.14.1"
"@typescript-eslint/scope-manager": "npm:7.13.1"
"@typescript-eslint/types": "npm:7.13.1"
"@typescript-eslint/typescript-estree": "npm:7.13.1"
peerDependencies:
eslint: ^8.56.0
checksum: 10/1ef74214ca84e32f151364512a51e82b7da5590dee03d0de0e1abcf18009e569f9a0638506cf03bd4a844af634b4935458e334b7b2459e9a50a67aba7d6228c7
checksum: 10/e1bc916dcb567c6b35819f635a84561e015f40b28d650b987f74c79b013ec43fb4f5b61199d4039fcdf9480281f945f622650cba2e68739600822da05808a706
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:7.14.1":
version: 7.14.1
resolution: "@typescript-eslint/visitor-keys@npm:7.14.1"
"@typescript-eslint/visitor-keys@npm:7.13.1":
version: 7.13.1
resolution: "@typescript-eslint/visitor-keys@npm:7.13.1"
dependencies:
"@typescript-eslint/types": "npm:7.14.1"
"@typescript-eslint/types": "npm:7.13.1"
eslint-visitor-keys: "npm:^3.4.3"
checksum: 10/42246f33cb3f9185c0b467c9a534e34a674e4fc08ba982a03aaa77dc1e569e916f1fca9ce9cd14c4df91f416e6e917bff51f98b8d8ca26ec5f67c253e8646bde
checksum: 10/811e9642851359b5197d45a9878143c4c608aaef887a20c26f57f8b012ce9e316d232b82a311bdd52a2af0c8b8da5d4bd9401ce565fc7bdb43cd44556e76d225
languageName: node
linkType: hard
@@ -5834,6 +5825,13 @@ __metadata:
languageName: node
linkType: hard
"axobject-query@npm:^2.2.0":
version: 2.2.0
resolution: "axobject-query@npm:2.2.0"
checksum: 10/25de4b5ba6b28f5856fab60d86ea20fea941586bc38f33c81b78d66cd7e9c5792a9b9a9e60a38407aa634e01fee6a34133fbbd1d1d3d24cc686de83c6bb1e634
languageName: node
linkType: hard
"b4a@npm:^1.6.4":
version: 1.6.6
resolution: "b4a@npm:1.6.6"
@@ -6276,6 +6274,25 @@ __metadata:
languageName: node
linkType: hard
"chokidar@npm:3.5.3":
version: 3.5.3
resolution: "chokidar@npm:3.5.3"
dependencies:
anymatch: "npm:~3.1.2"
braces: "npm:~3.0.2"
fsevents: "npm:~2.3.2"
glob-parent: "npm:~5.1.2"
is-binary-path: "npm:~2.1.0"
is-glob: "npm:~4.0.1"
normalize-path: "npm:~3.0.0"
readdirp: "npm:~3.6.0"
dependenciesMeta:
fsevents:
optional: true
checksum: 10/863e3ff78ee7a4a24513d2a416856e84c8e4f5e60efbe03e8ab791af1a183f569b62fc6f6b8044e2804966cb81277ddbbc1dc374fba3265bd609ea8efd62f5b3
languageName: node
linkType: hard
"chokidar@npm:^3.4.3, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
@@ -7581,13 +7598,13 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-lit-a11y@npm:4.1.3":
version: 4.1.3
resolution: "eslint-plugin-lit-a11y@npm:4.1.3"
"eslint-plugin-lit-a11y@npm:4.1.2":
version: 4.1.2
resolution: "eslint-plugin-lit-a11y@npm:4.1.2"
dependencies:
"@thepassle/axobject-query": "npm:^4.0.0"
aria-query: "npm:^5.1.3"
axe-core: "npm:^4.3.3"
axobject-query: "npm:^2.2.0"
dom5: "npm:^3.0.1"
emoji-regex: "npm:^10.2.1"
eslint-plugin-lit: "npm:^1.10.1"
@@ -7598,7 +7615,7 @@ __metadata:
requireindex: "npm:~1.2.0"
peerDependencies:
eslint: ">= 5"
checksum: 10/730a82cfefbeba87e604172db8c29fc18d3361b5c913531c05e83af26edbe612df955d5f124daf6066c3703b0e25f2352a8ea9cae8b2f8a3e6121937c297d3a9
checksum: 10/2d70f0b9fa6afc7f259877acd7e69c14f0104a69a019efb594d5de603e12b982e4a96fec5b169005fab244655951f85bff77f469d9aeadb885974f963a7d9996
languageName: node
linkType: hard
@@ -8895,7 +8912,7 @@ __metadata:
"@babel/preset-typescript": "npm:7.24.7"
"@babel/runtime": "npm:7.24.7"
"@braintree/sanitize-url": "npm:7.0.3"
"@bundle-stats/plugin-webpack-filter": "npm:4.13.3"
"@bundle-stats/plugin-webpack-filter": "npm:4.13.2"
"@codemirror/autocomplete": "npm:6.16.3"
"@codemirror/commands": "npm:6.6.0"
"@codemirror/language": "npm:6.10.2"
@@ -8951,7 +8968,7 @@ __metadata:
"@material/mwc-top-app-bar": "npm:0.27.0"
"@material/mwc-top-app-bar-fixed": "npm:0.27.0"
"@material/top-app-bar": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/web": "npm:1.5.1"
"@material/web": "npm:1.5.0"
"@mdi/js": "npm:7.4.47"
"@mdi/svg": "npm:7.4.47"
"@octokit/auth-oauth-device": "npm:7.1.1"
@@ -8969,7 +8986,7 @@ __metadata:
"@rollup/plugin-replace": "npm:5.0.7"
"@thomasloven/round-slider": "npm:0.6.0"
"@types/babel__plugin-transform-runtime": "npm:7.9.5"
"@types/chromecast-caf-receiver": "npm:6.0.16"
"@types/chromecast-caf-receiver": "npm:6.0.15"
"@types/chromecast-caf-sender": "npm:1.0.10"
"@types/color-name": "npm:1.1.4"
"@types/glob": "npm:8.1.0"
@@ -8979,15 +8996,15 @@ __metadata:
"@types/leaflet-draw": "npm:1.0.11"
"@types/lodash.merge": "npm:4.6.9"
"@types/luxon": "npm:3.4.2"
"@types/mocha": "npm:10.0.7"
"@types/mocha": "npm:10.0.6"
"@types/qrcode": "npm:1.5.5"
"@types/serve-handler": "npm:6.1.4"
"@types/sortablejs": "npm:1.15.8"
"@types/tar": "npm:6.1.13"
"@types/ua-parser-js": "npm:0.7.39"
"@types/webspeechapi": "npm:0.0.29"
"@typescript-eslint/eslint-plugin": "npm:7.14.1"
"@typescript-eslint/parser": "npm:7.14.1"
"@typescript-eslint/eslint-plugin": "npm:7.13.1"
"@typescript-eslint/parser": "npm:7.13.1"
"@vaadin/combo-box": "npm:24.4.0"
"@vaadin/vaadin-themable-mixin": "npm:24.4.0"
"@vibrant/color": "npm:3.2.1-alpha.1"
@@ -9020,7 +9037,7 @@ __metadata:
eslint-import-resolver-webpack: "npm:0.13.8"
eslint-plugin-import: "npm:2.29.1"
eslint-plugin-lit: "npm:1.14.0"
eslint-plugin-lit-a11y: "npm:4.1.3"
eslint-plugin-lit-a11y: "npm:4.1.2"
eslint-plugin-unused-imports: "npm:4.0.0"
eslint-plugin-wc: "npm:2.1.0"
fancy-log: "npm:2.0.0"
@@ -9053,7 +9070,7 @@ __metadata:
map-stream: "npm:0.0.7"
marked: "npm:12.0.2"
memoize-one: "npm:6.0.0"
mocha: "npm:10.5.0"
mocha: "npm:10.4.0"
node-vibrant: "npm:3.2.1-alpha.1"
object-hash: "npm:3.0.0"
open: "npm:10.1.0"
@@ -9083,7 +9100,7 @@ __metadata:
ts-lit-plugin: "npm:2.0.2"
tsparticles-engine: "npm:2.12.0"
tsparticles-preset-links: "npm:2.12.0"
typescript: "npm:5.5.2"
typescript: "npm:5.4.5"
ua-parser-js: "npm:1.0.38"
unfetch: "npm:5.0.0"
vis-data: "npm:7.1.9"
@@ -11152,13 +11169,13 @@ __metadata:
languageName: node
linkType: hard
"mocha@npm:10.5.0":
version: 10.5.0
resolution: "mocha@npm:10.5.0"
"mocha@npm:10.4.0":
version: 10.4.0
resolution: "mocha@npm:10.4.0"
dependencies:
ansi-colors: "npm:4.1.1"
browser-stdout: "npm:1.3.1"
chokidar: "npm:^3.5.3"
chokidar: "npm:3.5.3"
debug: "npm:4.3.4"
diff: "npm:5.0.0"
escape-string-regexp: "npm:4.0.0"
@@ -11179,7 +11196,7 @@ __metadata:
bin:
_mocha: bin/_mocha
mocha: bin/mocha.js
checksum: 10/64f5aace4a5b7eed23d8bcfaf1eacdaafbffdc5e3ee977ecb32ace8f91733b1520b8342ce02d7101e8d7b8bd4008e055e4f4f63a0e4a3da81baf0e41566a07ab
checksum: 10/0147b2a86c8a3b134b3bda949006aa5f2b08db606b9394e38eb3fa0d97dd2f54f06eb4afb270d4ae08aa6fb7674282737ed556b9a8bc407f9b8488380852eca4
languageName: node
linkType: hard
@@ -14209,13 +14226,13 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:5.5.2":
version: 5.5.2
resolution: "typescript@npm:5.5.2"
"typescript@npm:5.4.5":
version: 5.4.5
resolution: "typescript@npm:5.4.5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10/9118b20f248e76b0dbff8737fef65dfa89d02668d4e633d2c5ceac99033a0ca5e8a1c1a53bc94da68e8f67677a88f318663dde859c9e9a09c1e116415daec2ba
checksum: 10/d04a9e27e6d83861f2126665aa8d84847e8ebabcea9125b9ebc30370b98cb38b5dff2508d74e2326a744938191a83a69aa9fddab41f193ffa43eabfdf3f190a5
languageName: node
linkType: hard
@@ -14229,13 +14246,13 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A5.5.2#optional!builtin<compat/typescript>":
version: 5.5.2
resolution: "typescript@patch:typescript@npm%3A5.5.2#optional!builtin<compat/typescript>::version=5.5.2&hash=379a07"
"typescript@patch:typescript@npm%3A5.4.5#optional!builtin<compat/typescript>":
version: 5.4.5
resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin<compat/typescript>::version=5.4.5&hash=5adc0c"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10/ac3145f65cf9e72ab29f2196e05d5816b355dc1a9195b9f010d285182a12457cfacd068be2dd22c877f88ebc966ac6e0e83f51c8586412b16499a27e3670ff4b
checksum: 10/760f7d92fb383dbf7dee2443bf902f4365db2117f96f875cf809167f6103d55064de973db9f78fe8f31ec08fff52b2c969aee0d310939c0a3798ec75d0bca2e1
languageName: node
linkType: hard