mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-16 04:39:28 +00:00
Compare commits
29 Commits
dashboard_
...
20240626.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a16cae0671 | ||
![]() |
8d0c4e4a52 | ||
![]() |
4b7526c8a3 | ||
![]() |
6267ab5ed3 | ||
![]() |
ae94231800 | ||
![]() |
7d28f3f585 | ||
![]() |
adea384f40 | ||
![]() |
55b66250f4 | ||
![]() |
182111912c | ||
![]() |
8a0924bf1f | ||
![]() |
94dc9308ea | ||
![]() |
f42a9ac070 | ||
![]() |
1acada625f | ||
![]() |
128dbbcfef | ||
![]() |
57d8544dbf | ||
![]() |
76daa2bb7f | ||
![]() |
9cbb51549b | ||
![]() |
f43319a3ae | ||
![]() |
e8eefaf1d3 | ||
![]() |
06d82a4925 | ||
![]() |
4991d52fcc | ||
![]() |
0b391eafcf | ||
![]() |
0bb34830f8 | ||
![]() |
4a8bb5034d | ||
![]() |
a8366c6416 | ||
![]() |
8ff8c01bba | ||
![]() |
52f3ff3306 | ||
![]() |
10a5c4dfb4 | ||
![]() |
1a2daf8a7a |
@@ -1,6 +1,8 @@
|
||||
import Fuse from "fuse.js";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
|
||||
import { StoreAddon } from "../../../src/data/supervisor/store";
|
||||
import { getStripDiacriticsFn } from "../../../src/util/fuse";
|
||||
|
||||
export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: IFuseOptions<StoreAddon> = {
|
||||
@@ -8,7 +10,8 @@ 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(filter).map((result) => result.item);
|
||||
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
||||
}
|
||||
|
@@ -178,7 +178,7 @@
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.6",
|
||||
"@types/mocha": "10.0.7",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/serve-handler": "6.1.4",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
@@ -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.4.5",
|
||||
"typescript": "5.5.2",
|
||||
"webpack": "5.92.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 KiB |
3
public/static/images/logo_x.svg
Normal file
3
public/static/images/logo_x.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<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>
|
After Width: | Height: | Size: 430 B |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240610.0"
|
||||
version = "20240626.1"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { stripDiacritics } from "../strip-diacritics";
|
||||
import { fuzzyScore } from "./filter";
|
||||
|
||||
/**
|
||||
@@ -19,10 +20,10 @@ export const fuzzySequentialMatch = (
|
||||
for (const word of item.strings) {
|
||||
const scores = fuzzyScore(
|
||||
filter,
|
||||
filter.toLowerCase(),
|
||||
stripDiacritics(filter.toLowerCase()),
|
||||
0,
|
||||
word,
|
||||
word.toLowerCase(),
|
||||
stripDiacritics(word.toLowerCase()),
|
||||
0,
|
||||
true
|
||||
);
|
||||
|
2
src/common/string/strip-diacritics.ts
Normal file
2
src/common/string/strip-diacritics.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const stripDiacritics = (str: string) =>
|
||||
str.normalize("NFD").replace(/[\u0300-\u036F]/g, "");
|
280
src/components/data-table/dialog-data-table-settings.ts
Normal file
280
src/components/data-table/dialog-data-table-settings.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
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;
|
||||
}
|
||||
}
|
@@ -65,6 +65,10 @@ export interface DataTableSortColumnData {
|
||||
valueColumn?: string;
|
||||
direction?: SortingDirection;
|
||||
groupable?: boolean;
|
||||
moveable?: boolean;
|
||||
hideable?: boolean;
|
||||
defaultHidden?: boolean;
|
||||
showNarrow?: boolean;
|
||||
}
|
||||
|
||||
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
@@ -79,6 +83,7 @@ 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;
|
||||
@@ -105,6 +110,8 @@ 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[] = [];
|
||||
@@ -145,6 +152,10 @@ 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 = "";
|
||||
@@ -235,6 +246,7 @@ export class HaDataTable extends LitElement {
|
||||
(column: ClonedDataTableColumnData) => {
|
||||
delete column.title;
|
||||
delete column.template;
|
||||
delete column.extraTemplate;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -272,12 +284,44 @@ export class HaDataTable extends LitElement {
|
||||
this._sortFilterData();
|
||||
}
|
||||
|
||||
if (properties.has("selectable")) {
|
||||
if (properties.has("selectable") || properties.has("hiddenColumns")) {
|
||||
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}>
|
||||
@@ -326,9 +370,14 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map(([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return "";
|
||||
${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;
|
||||
}
|
||||
const sorted = key === this.sortColumn;
|
||||
const classes = {
|
||||
@@ -399,7 +448,7 @@ export class HaDataTable extends LitElement {
|
||||
@scroll=${this._saveScrollPos}
|
||||
.items=${this._items}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderRow}
|
||||
.renderItem=${renderRow}
|
||||
></lit-virtualizer>
|
||||
`}
|
||||
</div>
|
||||
@@ -409,7 +458,12 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row;
|
||||
|
||||
private _renderRow = (row: DataTableRowData, index: number) => {
|
||||
private _renderRow = (
|
||||
columns: DataTableColumnContainer,
|
||||
narrow: boolean,
|
||||
row: DataTableRowData,
|
||||
index: number
|
||||
) => {
|
||||
// not sure how this happens...
|
||||
if (!row) {
|
||||
return nothing;
|
||||
@@ -454,8 +508,14 @@ export class HaDataTable extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map(([key, column]) => {
|
||||
if (column.hidden) {
|
||||
${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)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
@@ -482,7 +542,38 @@ export class HaDataTable extends LitElement {
|
||||
})
|
||||
: ""}
|
||||
>
|
||||
${column.template ? column.template(row) : row[key]}
|
||||
${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}`}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@@ -861,6 +952,7 @@ export class HaDataTable extends LitElement {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mdc-data-table__cell {
|
||||
|
26
src/components/data-table/show-dialog-data-table-settings.ts
Normal file
26
src/components/data-table/show-dialog-data-table-settings.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { DataTableColumnContainer } from "./ha-data-table";
|
||||
|
||||
export interface DataTableSettingsDialogParams {
|
||||
columns: DataTableColumnContainer;
|
||||
onUpdate: (
|
||||
columnOrder: string[] | undefined,
|
||||
hiddenColumns: string[] | undefined
|
||||
) => void;
|
||||
hiddenColumns?: string[];
|
||||
columnOrder?: string[];
|
||||
}
|
||||
|
||||
export const loadDataTableSettingsDialog = () =>
|
||||
import("./dialog-data-table-settings");
|
||||
|
||||
export const showDataTableSettingsDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: DataTableSettingsDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-data-table-settings",
|
||||
dialogImport: loadDataTableSettingsDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -1,5 +1,6 @@
|
||||
import { expose } from "comlink";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
DataTableRowData,
|
||||
@@ -12,20 +13,18 @@ const filterData = (
|
||||
columns: SortableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
filter = filter.toUpperCase();
|
||||
filter = stripDiacritics(filter.toLowerCase());
|
||||
return data.filter((row) =>
|
||||
Object.entries(columns).some((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
if (column.filterable) {
|
||||
if (
|
||||
String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
)
|
||||
.toUpperCase()
|
||||
.includes(filter)
|
||||
) {
|
||||
const value = String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
);
|
||||
|
||||
if (stripDiacritics(value).toLowerCase().includes(filter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -90,7 +90,8 @@ class HaAnsiToHtml extends LitElement {
|
||||
|
||||
private _parseTextToColoredPre(text) {
|
||||
const pre = document.createElement("pre");
|
||||
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
|
||||
let i = 0;
|
||||
|
||||
const state: State = {
|
||||
|
@@ -65,6 +65,8 @@ 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[];
|
||||
@@ -247,20 +249,7 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
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"]
|
||||
) {
|
||||
private _getTargetedEntities = memoizeOne((target, value) => {
|
||||
const targetSelector = target ? { target } : { target: {} };
|
||||
const targetEntities =
|
||||
ensureArray(
|
||||
@@ -330,6 +319,13 @@ export class HaServiceControl extends LitElement {
|
||||
);
|
||||
});
|
||||
}
|
||||
return targetEntities;
|
||||
});
|
||||
|
||||
private _filterField(
|
||||
filter: ExtHassService["fields"][number]["filter"],
|
||||
targetEntities: string[]
|
||||
) {
|
||||
if (!targetEntities.length) {
|
||||
return false;
|
||||
}
|
||||
@@ -391,7 +387,10 @@ export class HaServiceControl extends LitElement {
|
||||
serviceData?.fields.some((field) => showOptionalToggle(field))
|
||||
);
|
||||
|
||||
const filteredFields = this._filterFields(serviceData, this._value);
|
||||
const targetEntities = this._getTargetedEntities(
|
||||
serviceData?.target,
|
||||
this._value
|
||||
);
|
||||
|
||||
const domain = this._value?.service
|
||||
? computeDomain(this._value.service)
|
||||
@@ -485,75 +484,115 @@ export class HaServiceControl extends LitElement {
|
||||
.defaultValue=${this._value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: 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>`
|
||||
: "";
|
||||
})} `;
|
||||
: 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
|
||||
)
|
||||
)} `;
|
||||
}
|
||||
|
||||
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 "";
|
||||
@@ -839,6 +878,11 @@ export class HaServiceControl extends LitElement {
|
||||
.description p {
|
||||
direction: ltr;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: 0;
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -352,6 +352,22 @@ export const saveAutomationConfig = (
|
||||
config: AutomationConfig
|
||||
) => hass.callApi<void>("POST", `config/automation/config/${id}`, config);
|
||||
|
||||
export const normalizeAutomationConfig = <
|
||||
T extends Partial<AutomationConfig> | AutomationConfig,
|
||||
>(
|
||||
config: T
|
||||
): T => {
|
||||
// Normalize data: ensure trigger, action and condition are lists
|
||||
// Happens when people copy paste their automations into the config
|
||||
for (const key of ["trigger", "condition", "action"]) {
|
||||
const value = config[key];
|
||||
if (value && !Array.isArray(value)) {
|
||||
config[key] = [value];
|
||||
}
|
||||
}
|
||||
return config;
|
||||
};
|
||||
|
||||
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
|
||||
initialAutomationEditorData = data;
|
||||
navigate("/config/automation/edit/new");
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { ManualAutomationConfig } from "./automation";
|
||||
import { ManualScriptConfig } from "./script";
|
||||
import { Selector } from "./selector";
|
||||
|
||||
export type BlueprintDomain = "automation" | "script";
|
||||
@@ -42,6 +44,11 @@ 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 });
|
||||
|
||||
@@ -91,3 +98,18 @@ 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,
|
||||
});
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiClose,
|
||||
mdiCog,
|
||||
mdiFilterVariant,
|
||||
mdiFilterVariantRemove,
|
||||
mdiFormatListChecks,
|
||||
@@ -42,6 +43,7 @@ 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 {
|
||||
@@ -171,6 +173,10 @@ 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;
|
||||
@@ -290,6 +296,14 @@ 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}
|
||||
@@ -416,6 +430,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
: ""}
|
||||
<ha-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.columns=${this.columns}
|
||||
.data=${this.data}
|
||||
.noDataText=${this.noDataText}
|
||||
@@ -430,6 +445,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.groupColumn=${this._groupColumn}
|
||||
.groupOrder=${this.groupOrder}
|
||||
.initialCollapsedGroups=${this.initialCollapsedGroups}
|
||||
.columnOrder=${this.columnOrder}
|
||||
.hiddenColumns=${this.hiddenColumns}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
@@ -438,7 +455,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
<div class="table-header">
|
||||
${this.hasFilters && !this.showFilters
|
||||
? html`${filterButton}`
|
||||
: nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}
|
||||
: nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
@@ -448,7 +465,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
${this.hasFilters && !this.showFilters
|
||||
? html`${filterButton}`
|
||||
: nothing}
|
||||
${selectModeBtn}${groupByMenu}${sortByMenu}
|
||||
${selectModeBtn}${groupByMenu}${sortByMenu}${settingsButton}
|
||||
</div>`}
|
||||
</ha-data-table>`}
|
||||
<div slot="fab"><slot name="fab"></slot></div>
|
||||
@@ -608,6 +625,22 @@ 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();
|
||||
}
|
||||
@@ -874,6 +907,10 @@ declare global {
|
||||
interface HASSDomEvents {
|
||||
"search-changed": { value: string };
|
||||
"grouping-changed": { value: string };
|
||||
"columns-changed": {
|
||||
columnOrder: string[] | undefined;
|
||||
hiddenColumns: string[] | undefined;
|
||||
};
|
||||
"clear-filter": undefined;
|
||||
}
|
||||
}
|
||||
|
@@ -72,11 +72,11 @@ class DialogCommunity extends LitElement {
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://twitter.com/home_assistant"
|
||||
href="https://x.com/home_assistant"
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<img src="/static/images/logo_twitter.png" slot="graphic" />
|
||||
${this.localize("ui.panel.page-onboarding.welcome.twitter")}
|
||||
<img class="x" src="/static/images/logo_x.svg" slot="graphic" />
|
||||
${this.localize("ui.panel.page-onboarding.welcome.x")}
|
||||
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
@@ -96,6 +96,12 @@ class DialogCommunity extends LitElement {
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
img.x {
|
||||
filter: invert(1) hue-rotate(180deg);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-help-tooltip";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import {
|
||||
ApplicationCredential,
|
||||
deleteApplicationCredential,
|
||||
@@ -70,6 +71,26 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
width: "30%",
|
||||
direction: "asc",
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
width: "64px",
|
||||
type: "overflow-menu",
|
||||
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._removeCredential(credential),
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
return columns;
|
||||
@@ -153,6 +174,24 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private _removeCredential = 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);
|
||||
};
|
||||
|
||||
private _removeSelected() {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -162,8 +201,9 @@ 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.remove"),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
|
@@ -18,6 +18,7 @@ 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";
|
||||
@@ -50,6 +51,7 @@ 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,
|
||||
@@ -208,9 +210,10 @@ 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(filter).map((result) => result.item);
|
||||
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
||||
}
|
||||
);
|
||||
|
||||
|
@@ -3,10 +3,10 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-markdown";
|
||||
import { BlueprintAutomationConfig } from "../../../data/automation";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
import "../../../components/ha-markdown";
|
||||
|
||||
@customElement("blueprint-automation-editor")
|
||||
export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
mdiDebugStepOver,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiFileEdit,
|
||||
mdiInformationOutline,
|
||||
mdiPlay,
|
||||
mdiPlayCircleOutline,
|
||||
@@ -40,10 +41,12 @@ import "../../../components/ha-yaml-editor";
|
||||
import {
|
||||
AutomationConfig,
|
||||
AutomationEntity,
|
||||
BlueprintAutomationConfig,
|
||||
deleteAutomation,
|
||||
fetchAutomationFileConfig,
|
||||
getAutomationEditorInitData,
|
||||
getAutomationStateConfig,
|
||||
normalizeAutomationConfig,
|
||||
saveAutomationConfig,
|
||||
showAutomationEditor,
|
||||
triggerAutomationActions,
|
||||
@@ -65,6 +68,7 @@ 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 {
|
||||
@@ -235,6 +239,24 @@ 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._mode === "yaml"}
|
||||
>
|
||||
${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}>
|
||||
@@ -432,7 +454,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
this._config = {
|
||||
...baseConfig,
|
||||
...initData,
|
||||
...(initData ? normalizeAutomationConfig(initData) : initData),
|
||||
} as AutomationConfig;
|
||||
this._entityId = undefined;
|
||||
this._readOnly = false;
|
||||
@@ -441,7 +463,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this._config = this._normalizeConfig(c.config);
|
||||
this._config = normalizeAutomationConfig(c.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
this._entityId = this.entityId;
|
||||
@@ -497,18 +519,6 @@ 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(
|
||||
@@ -517,7 +527,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
);
|
||||
this._dirty = false;
|
||||
this._readOnly = false;
|
||||
this._config = this._normalizeConfig(config);
|
||||
this._config = normalizeAutomationConfig(config);
|
||||
this._checkValidation();
|
||||
} catch (err: any) {
|
||||
const entityRegistry = await fetchEntityRegistry(this.hass.connection);
|
||||
@@ -638,6 +648,45 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
};
|
||||
|
||||
private async _takeControl() {
|
||||
const config = this._config as BlueprintAutomationConfig;
|
||||
|
||||
const confirmation = await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.automation.editor.take_control_confirmation.title"
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.automation.editor.take_control_confirmation.text"
|
||||
),
|
||||
confirmText: this.hass!.localize(
|
||||
"ui.panel.config.automation.editor.take_control_confirmation.action"
|
||||
),
|
||||
});
|
||||
|
||||
if (!confirmation) return;
|
||||
|
||||
try {
|
||||
const result = await substituteBlueprint(
|
||||
this.hass,
|
||||
"automation",
|
||||
config.use_blueprint.path,
|
||||
config.use_blueprint.input || {}
|
||||
);
|
||||
|
||||
const newConfig = {
|
||||
...normalizeAutomationConfig(result.substituted_config),
|
||||
alias: config.alias,
|
||||
description: config.description,
|
||||
};
|
||||
|
||||
this._config = newConfig;
|
||||
this._dirty = true;
|
||||
this._errors = undefined;
|
||||
} catch (err: any) {
|
||||
this._errors = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private async _duplicate() {
|
||||
const result = this._readOnly
|
||||
? await showConfirmationDialog(this, {
|
||||
|
@@ -192,6 +192,20 @@ 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, {
|
||||
@@ -253,6 +267,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.automation.picker.headers.state"),
|
||||
type: "icon",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
template: (automation) =>
|
||||
html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
@@ -272,30 +288,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
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}
|
||||
`;
|
||||
},
|
||||
extraTemplate: (automation) =>
|
||||
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"),
|
||||
@@ -322,7 +321,6 @@ 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");
|
||||
@@ -341,9 +339,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
|
||||
@@ -356,6 +354,9 @@ 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}
|
||||
@@ -545,6 +546,9 @@ 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}
|
||||
@@ -1415,6 +1419,11 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -60,14 +60,19 @@ class HaConfigBackup extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (backup) =>
|
||||
html`${backup.name}
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
template: narrow
|
||||
? undefined
|
||||
: (backup) =>
|
||||
html`${backup.name}
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
},
|
||||
path: {
|
||||
title: localize("ui.panel.config.backup.path"),
|
||||
hidden: !narrow,
|
||||
},
|
||||
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",
|
||||
@@ -76,7 +81,6 @@ class HaConfigBackup extends LitElement {
|
||||
title: localize("ui.panel.config.backup.created"),
|
||||
width: "15%",
|
||||
direction: "desc",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (backup) =>
|
||||
@@ -87,6 +91,9 @@ 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}
|
||||
|
@@ -107,6 +107,20 @@ 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",
|
||||
@@ -154,8 +168,6 @@ class HaBlueprintOverview extends LitElement {
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow,
|
||||
_language,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<BlueprintMetaDataPath> => ({
|
||||
name: {
|
||||
@@ -165,19 +177,12 @@ 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%",
|
||||
},
|
||||
@@ -185,7 +190,6 @@ class HaBlueprintOverview extends LitElement {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.file_name"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
hidden: narrow,
|
||||
direction: "asc",
|
||||
width: "25%",
|
||||
},
|
||||
@@ -197,6 +201,9 @@ 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
|
||||
@@ -280,11 +287,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
|
||||
id="fullpath"
|
||||
.noDataText=${this.hass.localize(
|
||||
@@ -313,6 +316,9 @@ 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}
|
||||
@@ -556,6 +562,11 @@ class HaBlueprintOverview extends LitElement {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyle;
|
||||
}
|
||||
|
@@ -61,32 +61,32 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
|
||||
href="https://community.home-assistant.io"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Forums</a
|
||||
>${hass.localize("ui.panel.config.tips.join_forums")}</a
|
||||
>`,
|
||||
twitter: html`<a
|
||||
href=${documentationUrl(hass, `/twitter`)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Twitter</a
|
||||
>${hass.localize("ui.panel.config.tips.join_x")}</a
|
||||
>`,
|
||||
discord: html`<a
|
||||
href=${documentationUrl(hass, `/join-chat`)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Chat</a
|
||||
>${hass.localize("ui.panel.config.tips.join_chat")}</a
|
||||
>`,
|
||||
blog: html`<a
|
||||
href=${documentationUrl(hass, `/blog`)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Blog</a
|
||||
>${hass.localize("ui.panel.config.tips.join_blog")}</a
|
||||
>`,
|
||||
newsletter: html`<span class="keep-together"
|
||||
><a
|
||||
href="https://newsletter.openhomefoundation.org/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Newsletter</a
|
||||
>${hass.localize("ui.panel.config.tips.join_newsletter")}</a
|
||||
>
|
||||
</span>`,
|
||||
}),
|
||||
|
@@ -154,6 +154,20 @@ 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,
|
||||
});
|
||||
@@ -434,10 +448,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
typeof this._devicesAndFilterDomains
|
||||
>["devicesOutput"][number];
|
||||
|
||||
const columns: DataTableColumnContainer<DeviceItem> = {
|
||||
return {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.devices.data_table.icon"),
|
||||
type: "icon",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
template: (device) =>
|
||||
device.domains.length
|
||||
? html`<img
|
||||
@@ -452,19 +469,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
/>`
|
||||
: "",
|
||||
},
|
||||
};
|
||||
|
||||
if (narrow) {
|
||||
columns.name = {
|
||||
name: {
|
||||
title: localize("ui.panel.config.devices.data_table.device"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (device) => html`
|
||||
<div style="font-size: 14px;">${device.name}</div>
|
||||
<div class="secondary">${device.area} | ${device.integration}</div>
|
||||
extraTemplate: (device) => html`
|
||||
${device.label_entries.length
|
||||
? html`
|
||||
<ha-data-table-labels
|
||||
@@ -473,112 +485,89 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
`
|
||||
: nothing}
|
||||
`,
|
||||
};
|
||||
} else {
|
||||
columns.name = {
|
||||
title: localize("ui.panel.config.devices.data_table.device"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (device) => html`
|
||||
<div style="font-size: 14px;">${device.name}</div>
|
||||
${device.label_entries.length
|
||||
? html`
|
||||
<ha-data-table-labels
|
||||
.labels=${device.label_entries}
|
||||
></ha-data-table-labels>
|
||||
`
|
||||
: 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`—`;
|
||||
},
|
||||
};
|
||||
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(" "),
|
||||
};
|
||||
manufacturer: {
|
||||
title: localize("ui.panel.config.devices.data_table.manufacturer"),
|
||||
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 columns;
|
||||
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>;
|
||||
});
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
@@ -704,6 +693,9 @@ 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}
|
||||
@@ -1043,6 +1035,11 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
|
@@ -186,6 +186,20 @@ 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;
|
||||
|
||||
@@ -251,15 +265,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
]);
|
||||
|
||||
private _columns = memoize(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
narrow,
|
||||
_language
|
||||
): DataTableColumnContainer<EntityRow> => ({
|
||||
(localize: LocalizeFunc): 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>`
|
||||
@@ -283,32 +295,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
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
|
||||
extraTemplate: (entry) =>
|
||||
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,
|
||||
@@ -324,7 +327,6 @@ 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%",
|
||||
@@ -343,6 +345,7 @@ 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",
|
||||
@@ -688,11 +691,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(
|
||||
this.hass.localize,
|
||||
this.narrow,
|
||||
this.hass.language
|
||||
)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${filteredEntities}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.search",
|
||||
@@ -714,6 +713,9 @@ 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}
|
||||
@@ -1335,6 +1337,11 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -167,6 +167,20 @@ 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>;
|
||||
@@ -243,14 +257,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow: boolean,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<HelperItem> => ({
|
||||
(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
|
||||
@@ -269,23 +282,17 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
grows: true,
|
||||
direction: "asc",
|
||||
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
|
||||
extraTemplate: (helper) =>
|
||||
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%",
|
||||
@@ -313,10 +320,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
editable: {
|
||||
title: "",
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.headers.editable"
|
||||
),
|
||||
label: localize("ui.panel.config.helpers.picker.headers.editable"),
|
||||
type: "icon",
|
||||
showNarrow: true,
|
||||
template: (helper) => html`
|
||||
${!helper.editable
|
||||
? html`
|
||||
@@ -337,8 +343,12 @@ 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}
|
||||
@@ -556,11 +566,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.columns=${this._columns(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}
|
||||
@@ -1084,6 +1097,11 @@ ${rejected
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -55,6 +55,8 @@ 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;
|
||||
@@ -255,6 +257,7 @@ 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,
|
||||
@@ -264,15 +267,16 @@ 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(filter)
|
||||
.search(normalizedFilter)
|
||||
.map((result) => result.item),
|
||||
...new Fuse(yamlIntegrations, options)
|
||||
.search(filter)
|
||||
.search(normalizedFilter)
|
||||
.map((result) => result.item),
|
||||
...new Fuse(helpers, options)
|
||||
.search(filter)
|
||||
.search(normalizedFilter)
|
||||
.map((result) => result.item),
|
||||
];
|
||||
}
|
||||
|
@@ -1,25 +1,27 @@
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiFilterVariant, mdiPlus } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} 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 {
|
||||
protocolIntegrationPicked,
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} 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";
|
||||
@@ -29,6 +31,7 @@ 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";
|
||||
@@ -37,11 +40,11 @@ import {
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
IntegrationLogInfo,
|
||||
IntegrationManifest,
|
||||
domainToName,
|
||||
fetchIntegrationManifest,
|
||||
fetchIntegrationManifests,
|
||||
IntegrationLogInfo,
|
||||
IntegrationManifest,
|
||||
subscribeLogInfo,
|
||||
} from "../../../data/integration";
|
||||
import {
|
||||
@@ -59,18 +62,17 @@ 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;
|
||||
@@ -208,9 +210,12 @@ 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(filter).map((result) => result.item);
|
||||
filteredEntries = fuse
|
||||
.search(stripDiacritics(filter))
|
||||
.map((result) => result.item);
|
||||
} else {
|
||||
filteredEntries = configEntriesInProgress;
|
||||
}
|
||||
|
@@ -66,10 +66,26 @@ 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) =>
|
||||
@@ -77,6 +93,7 @@ export class HaConfigLabels extends LitElement {
|
||||
},
|
||||
color: {
|
||||
title: "",
|
||||
showNarrow: true,
|
||||
label: localize("ui.panel.config.labels.headers.color"),
|
||||
type: "icon",
|
||||
template: (label) =>
|
||||
@@ -105,6 +122,9 @@ export class HaConfigLabels extends LitElement {
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
hideable: false,
|
||||
width: "64px",
|
||||
type: "overflow-menu",
|
||||
template: (label) => html`
|
||||
@@ -167,6 +187,9 @@ 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}
|
||||
@@ -297,6 +320,11 @@ export class HaConfigLabels extends LitElement {
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -2,21 +2,17 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiCheckCircleOutline,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiPencil,
|
||||
mdiOpenInNew,
|
||||
mdiPlus,
|
||||
mdiStar,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, PropertyValues, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, 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,
|
||||
@@ -26,9 +22,6 @@ 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 {
|
||||
@@ -48,11 +41,13 @@ 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,
|
||||
@@ -90,9 +85,19 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@state() private _overflowDashboard?: LovelaceDashboard;
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-column-order",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeColumnOrder?: string[];
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMenu;
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-hidden-columns",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
@@ -110,6 +115,8 @@ 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"
|
||||
),
|
||||
@@ -137,118 +144,111 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
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;
|
||||
},
|
||||
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>
|
||||
`
|
||||
: ""}
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
if (!narrow) {
|
||||
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 = {
|
||||
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.actions = {
|
||||
title: "",
|
||||
width: "64px",
|
||||
type: "icon-button",
|
||||
columns.mode = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
template: (dashboard) => html`
|
||||
<ha-icon-button
|
||||
.dashboard=${dashboard}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
@click=${this._showOverflowMenu}
|
||||
></ha-icon-button>
|
||||
${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",
|
||||
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 = {
|
||||
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
|
||||
>
|
||||
`,
|
||||
};
|
||||
|
||||
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,10 +316,13 @@ 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._navigate}
|
||||
@row-click=${this._editDashboard}
|
||||
id="url_path"
|
||||
hasFab
|
||||
clickable
|
||||
@@ -351,22 +354,6 @@ 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>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -379,23 +366,21 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._dashboards = await fetchDashboards(this.hass);
|
||||
}
|
||||
|
||||
private _navigate(ev: CustomEvent) {
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
navigate(`/${urlPath}`);
|
||||
private _navigate(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
navigate(`/${(ev.target as any).urlPath}`);
|
||||
}
|
||||
|
||||
private _editDashboard = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const dashboard = ev.currentTarget.parentElement.anchorElement.automation;
|
||||
|
||||
const urlPath = (ev.currentTarget as any).urlPath;
|
||||
private _editDashboard(ev: CustomEvent) {
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
|
||||
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, {
|
||||
@@ -490,6 +475,11 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -67,12 +67,27 @@ 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"
|
||||
),
|
||||
@@ -145,6 +160,9 @@ 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}
|
||||
@@ -266,6 +284,11 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
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";
|
||||
@@ -137,11 +140,17 @@ class DialogPersonDetail extends LitElement {
|
||||
@change=${this._pictureChanged}
|
||||
></ha-picture-upload>
|
||||
|
||||
<ha-formfield
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.config.person.detail.allow_login"
|
||||
)}${this._user ? ` (${this._user.username})` : ""}`}
|
||||
>
|
||||
<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-switch
|
||||
@change=${this._allowLoginChanged}
|
||||
.disabled=${this._user &&
|
||||
@@ -150,34 +159,9 @@ class DialogPersonDetail extends LitElement {
|
||||
this._user.is_owner)}
|
||||
.checked=${this._userId}
|
||||
></ha-switch>
|
||||
</ha-formfield>
|
||||
</ha-settings-row>
|
||||
|
||||
${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._renderUserFields()}
|
||||
${this._deviceTrackersAvailable(this.hass)
|
||||
? html`
|
||||
<p>
|
||||
@@ -235,7 +219,7 @@ class DialogPersonDetail extends LitElement {
|
||||
</div>
|
||||
${this._params.entry
|
||||
? html`
|
||||
<mwc-button
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click=${this._deleteEntry}
|
||||
@@ -243,28 +227,10 @@ class DialogPersonDetail extends LitElement {
|
||||
this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.person.detail.delete")}
|
||||
</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>`
|
||||
: ""}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
<mwc-button
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
@@ -272,11 +238,96 @@ 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")}
|
||||
</mwc-button>
|
||||
</ha-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;
|
||||
}
|
||||
@@ -317,14 +368,16 @@ 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",
|
||||
"ui.panel.config.person.detail.confirm_delete_user_text",
|
||||
{ name: this._name }
|
||||
),
|
||||
confirmText: this.hass!.localize(
|
||||
"ui.panel.config.person.detail.delete"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
target.checked = true;
|
||||
@@ -488,9 +541,8 @@ class DialogPersonDetail extends LitElement {
|
||||
margin-bottom: 16px;
|
||||
--file-upload-image-border-radius: 50%;
|
||||
}
|
||||
ha-formfield {
|
||||
display: block;
|
||||
padding: 16px 0;
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
|
@@ -180,6 +180,20 @@ 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,
|
||||
});
|
||||
@@ -225,11 +239,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow, localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
(localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<SceneItem> = {
|
||||
icon: {
|
||||
title: "",
|
||||
label: localize("ui.panel.config.scene.picker.headers.state"),
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
type: "icon",
|
||||
template: (scene) => html`
|
||||
<ha-state-icon
|
||||
@@ -245,15 +261,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (scene) => html`
|
||||
<div style="font-size: 14px;">${scene.name}</div>
|
||||
${scene.labels.length
|
||||
extraTemplate: (scene) =>
|
||||
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"),
|
||||
@@ -281,7 +295,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
sortable: true,
|
||||
width: "30%",
|
||||
hidden: narrow,
|
||||
template: (scene) => {
|
||||
const lastActivated = scene.state;
|
||||
if (!lastActivated || isUnavailableState(lastActivated)) {
|
||||
@@ -300,6 +313,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
only_editable: {
|
||||
title: "",
|
||||
width: "56px",
|
||||
showNarrow: true,
|
||||
template: (scene) =>
|
||||
!scene.attributes.id
|
||||
? html`
|
||||
@@ -319,6 +333,9 @@ 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}
|
||||
@@ -536,11 +553,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.columns=${this._columns(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}
|
||||
@@ -1155,6 +1175,11 @@ ${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,
|
||||
|
@@ -2,10 +2,10 @@ import "@material/mwc-button/mwc-button";
|
||||
import { html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
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";
|
||||
import { fetchBlueprints } from "../../../data/blueprint";
|
||||
import { BlueprintScriptConfig } from "../../../data/script";
|
||||
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
|
||||
|
||||
@customElement("blueprint-script-editor")
|
||||
export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
mdiDebugStepOver,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiFileEdit,
|
||||
mdiFormTextbox,
|
||||
mdiInformationOutline,
|
||||
mdiPlay,
|
||||
@@ -40,6 +41,7 @@ import { validateConfig } from "../../../data/config";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import {
|
||||
BlueprintScriptConfig,
|
||||
ScriptConfig,
|
||||
deleteScript,
|
||||
fetchScriptFileConfig,
|
||||
@@ -61,6 +63,7 @@ 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;
|
||||
@@ -228,6 +231,24 @@ 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._mode === "yaml"}
|
||||
>
|
||||
${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}>
|
||||
@@ -601,6 +622,45 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
};
|
||||
|
||||
private async _takeControl() {
|
||||
const config = this._config as BlueprintScriptConfig;
|
||||
|
||||
const confirmation = await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.script.editor.take_control_confirmation.title"
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.config.script.editor.take_control_confirmation.text"
|
||||
),
|
||||
confirmText: this.hass!.localize(
|
||||
"ui.panel.config.script.editor.take_control_confirmation.action"
|
||||
),
|
||||
});
|
||||
|
||||
if (!confirmation) return;
|
||||
|
||||
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._config = newConfig;
|
||||
this._dirty = true;
|
||||
this._errors = undefined;
|
||||
} catch (err: any) {
|
||||
this._errors = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private async _duplicate() {
|
||||
const result = this._readOnly
|
||||
? await showConfirmationDialog(this, {
|
||||
|
@@ -184,6 +184,20 @@ 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,
|
||||
});
|
||||
@@ -232,14 +246,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow,
|
||||
localize: LocalizeFunc,
|
||||
locale: HomeAssistant["locale"]
|
||||
): DataTableColumnContainer<ScriptItem> => {
|
||||
(localize: LocalizeFunc): DataTableColumnContainer<ScriptItem> => {
|
||||
const columns: DataTableColumnContainer = {
|
||||
icon: {
|
||||
title: "",
|
||||
showNarrow: true,
|
||||
moveable: false,
|
||||
label: localize("ui.panel.config.script.picker.headers.state"),
|
||||
type: "icon",
|
||||
template: (script) =>
|
||||
@@ -259,30 +271,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
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}
|
||||
`;
|
||||
},
|
||||
extraTemplate: (script) =>
|
||||
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"),
|
||||
@@ -305,7 +300,6 @@ 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"),
|
||||
@@ -330,6 +324,9 @@ 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}
|
||||
@@ -539,6 +536,9 @@ 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,11 +553,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.localize,
|
||||
this.hass.locale
|
||||
)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${scripts}
|
||||
.empty=${!this.scripts.length}
|
||||
.activeFilters=${this._activeFilters}
|
||||
@@ -1270,6 +1266,11 @@ ${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,
|
||||
|
@@ -66,93 +66,82 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
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 = {
|
||||
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<TagRowData> = {
|
||||
icon: {
|
||||
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._handleAutomationClick}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.tag.create_automation"
|
||||
)}
|
||||
.path=${mdiRobot}
|
||||
@click=${this._handleWriteClick}
|
||||
.label=${this.hass.localize("ui.panel.config.tag.write")}
|
||||
.path=${mdiContentDuplicate}
|
||||
></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) => ({
|
||||
@@ -191,11 +180,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.tags}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._data(this._tags)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")}
|
||||
.filter=${this._filter}
|
||||
|
@@ -1,31 +1,34 @@
|
||||
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 {
|
||||
@@ -155,38 +158,44 @@ export class DialogAddUser extends LitElement {
|
||||
"ui.panel.config.users.add_user.password_not_match"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_only"
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_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
|
||||
.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>
|
||||
</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>
|
||||
${!this._isAdmin
|
||||
? html`
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
${this._loading
|
||||
? html`
|
||||
@@ -195,7 +204,7 @@ export class DialogAddUser extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._name ||
|
||||
!this._username ||
|
||||
@@ -204,7 +213,7 @@ export class DialogAddUser extends LitElement {
|
||||
@click=${this._createUser}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.users.add_user.create")}
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -299,7 +308,10 @@ export class DialogAddUser extends LitElement {
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -132,7 +132,7 @@ class DialogAdminChangePassword extends LitElement {
|
||||
@value-changed=${this._valueChanged}
|
||||
.disabled=${this._submitting}
|
||||
></ha-form>
|
||||
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
|
||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
|
@@ -1,12 +1,13 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import "@material/mwc-button";
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
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-help-tooltip";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-label";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
@@ -68,15 +69,15 @@ class DialogUserDetail extends LitElement {
|
||||
.heading=${createCloseHeading(this.hass, user.name)}
|
||||
>
|
||||
<div>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${this._error
|
||||
? html`<div class="error">${this._error}</div>`
|
||||
: nothing}
|
||||
<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(
|
||||
@@ -90,74 +91,136 @@ class DialogUserDetail extends LitElement {
|
||||
</div>
|
||||
`}
|
||||
<div class="form">
|
||||
<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"
|
||||
)}
|
||||
>
|
||||
<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"
|
||||
)}
|
||||
>
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isAdmin}
|
||||
@change=${this._adminChanged}
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
${!this._isAdmin
|
||||
${!user.system_generated
|
||||
? html`
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.users_privileges_note"
|
||||
)}
|
||||
<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>
|
||||
`
|
||||
: ""}
|
||||
<div class="row">
|
||||
<ha-formfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.users.editor.active"
|
||||
: 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"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
.disabled=${user.system_generated || user.is_owner}
|
||||
.checked=${this._isActive}
|
||||
@change=${this._activeChanged}
|
||||
>
|
||||
<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-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.local_access_only"
|
||||
)}
|
||||
</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-help-tooltip>
|
||||
</div>
|
||||
</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
|
||||
? 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">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.system_generated_read_only_users"
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<div slot="secondaryAction">
|
||||
<mwc-button
|
||||
<ha-button
|
||||
class="warning"
|
||||
@click=${this._deleteEntry}
|
||||
.disabled=${this._submitting ||
|
||||
@@ -165,47 +228,18 @@ class DialogUserDetail extends LitElement {
|
||||
user.is_owner}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.users.editor.delete_user")}
|
||||
</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>`
|
||||
: ""}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<div slot="primaryAction">
|
||||
<mwc-button
|
||||
<ha-button
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!this._name ||
|
||||
this._submitting ||
|
||||
user.system_generated}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.users.editor.update_user")}
|
||||
</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>
|
||||
`
|
||||
: ""}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
@@ -353,27 +387,8 @@ class DialogUserDetail extends LitElement {
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: 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;
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -46,6 +46,20 @@ 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",
|
||||
@@ -72,17 +86,6 @@ 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"),
|
||||
@@ -90,7 +93,6 @@ export class HaConfigUsers extends LitElement {
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
template: (user) => html`${user.username || "—"}`,
|
||||
},
|
||||
group: {
|
||||
@@ -100,7 +102,6 @@ export class HaConfigUsers extends LitElement {
|
||||
groupable: true,
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
},
|
||||
is_active: {
|
||||
title: this.hass.localize(
|
||||
@@ -154,6 +155,7 @@ 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(
|
||||
@@ -186,6 +188,9 @@ 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}
|
||||
@@ -213,6 +218,7 @@ 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]}`),
|
||||
}))
|
||||
);
|
||||
@@ -302,6 +308,11 @@ export class HaConfigUsers extends LitElement {
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -118,6 +118,20 @@ 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;
|
||||
|
||||
@@ -137,6 +151,7 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
icon: {
|
||||
title: "",
|
||||
type: "icon",
|
||||
moveable: false,
|
||||
hidden: narrow,
|
||||
template: (entry) => html`
|
||||
<ha-state-icon
|
||||
@@ -153,10 +168,20 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
grows: true,
|
||||
template: (entry) => html`
|
||||
${entry.name}<br />
|
||||
<div class="secondary">${entry.entity_id}</div>
|
||||
`,
|
||||
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,
|
||||
},
|
||||
domain: {
|
||||
title: localize(
|
||||
@@ -171,7 +196,6 @@ 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%",
|
||||
},
|
||||
@@ -179,6 +203,7 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
title: localize(
|
||||
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||
),
|
||||
showNarrow: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
width: "160px",
|
||||
@@ -208,7 +233,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
hidden: narrow,
|
||||
width: "15%",
|
||||
template: (entry) =>
|
||||
entry.aliases.length === 0
|
||||
@@ -230,12 +254,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
.path=${mdiCloseCircleOutline}
|
||||
></ha-icon-button>`,
|
||||
},
|
||||
// For search
|
||||
entity_id: {
|
||||
title: "",
|
||||
hidden: true,
|
||||
filterable: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -552,6 +570,9 @@ 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}
|
||||
@@ -757,6 +778,11 @@ 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,
|
||||
|
@@ -105,6 +105,8 @@ export class HuiCard extends ReactiveElement {
|
||||
"ll-upgrade",
|
||||
(ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
fireEvent(this, "card-updated");
|
||||
},
|
||||
{ once: true }
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@@ -26,6 +27,15 @@ 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");
|
||||
@@ -123,16 +133,25 @@ 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>
|
||||
<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>
|
||||
<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-icon-button
|
||||
class="more-info"
|
||||
.label=${this.hass!.localize(
|
||||
@@ -156,10 +175,15 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
:host {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -178,13 +202,28 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
ha-state-control-humidifier-humidity {
|
||||
width: 100%;
|
||||
max-width: 344px; /* 12px + 12px + 320px */
|
||||
padding: 0 12px 12px 12px;
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.container:before {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
@@ -201,6 +240,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
|
||||
|
||||
hui-card-features {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@@ -18,14 +19,23 @@ 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 "../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");
|
||||
@@ -115,16 +125,25 @@ 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>
|
||||
<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>
|
||||
<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-icon-button
|
||||
class="more-info"
|
||||
.label=${this.hass!.localize(
|
||||
@@ -148,10 +167,15 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
:host {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -170,13 +194,28 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
ha-state-control-climate-temperature {
|
||||
width: 100%;
|
||||
max-width: 344px; /* 12px + 12px + 320px */
|
||||
padding: 0 12px 12px 12px;
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.container:before {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
@@ -193,6 +232,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
|
||||
hui-card-features {
|
||||
width: 100%;
|
||||
flex: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@@ -14,7 +15,6 @@ 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";
|
||||
@@ -74,10 +74,21 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _subscribed?: Promise<() => void>;
|
||||
|
||||
// @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 _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";
|
||||
},
|
||||
});
|
||||
|
||||
private _needForecastSubscription() {
|
||||
return (
|
||||
@@ -118,14 +129,10 @@ 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();
|
||||
}
|
||||
|
||||
@@ -159,16 +166,6 @@ 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) {
|
||||
@@ -226,7 +223,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
const forecast =
|
||||
this._config?.show_forecast !== false && forecastData?.forecast?.length
|
||||
? forecastData.forecast.slice(0, this.veryVeryNarrow ? 3 : 5)
|
||||
? forecastData.forecast.slice(
|
||||
0,
|
||||
this._sizeController.value === "very-very-narrow" ? 3 : 5
|
||||
)
|
||||
: undefined;
|
||||
const weather = !forecast || this._config?.show_current !== false;
|
||||
|
||||
@@ -238,6 +238,7 @@ 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),
|
||||
@@ -416,44 +417,6 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private _measureCard() {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -462,6 +425,11 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
return [
|
||||
weatherSVGStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
@@ -612,48 +580,48 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
/* ============= NARROW ============= */
|
||||
|
||||
:host([narrow]) .icon-image {
|
||||
[class*="narrow"] .icon-image {
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
:host([narrow]) .weather-image {
|
||||
[class*="narrow"] .weather-image {
|
||||
flex: 0 0 52px;
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
:host([narrow]) .icon-image .weather-icon {
|
||||
[class*="narrow"] .icon-image .weather-icon {
|
||||
--mdc-icon-size: 52px;
|
||||
}
|
||||
|
||||
:host([narrow]) .state,
|
||||
:host([narrow]) .temp-attribute .temp {
|
||||
[class*="narrow"] .state,
|
||||
[class*="narrow"] .temp-attribute .temp {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
:host([narrow]) .temp-attribute .temp {
|
||||
[class*="narrow"] .temp-attribute .temp {
|
||||
margin-right: 16px;
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
|
||||
:host([narrow]) .temp span {
|
||||
[class*="narrow"] .temp span {
|
||||
top: 1px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* ============= VERY NARROW ============= */
|
||||
|
||||
:host([veryNarrow]) .name,
|
||||
:host([veryNarrow]) .attribute {
|
||||
[class*="very-narrow"] .name,
|
||||
[class*="very-narrow"] .attribute {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([veryNarrow]) .info {
|
||||
[class*="very-narrow"] .info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
:host([veryNarrow]) .name-state {
|
||||
[class*="very-narrow"] .name-state {
|
||||
padding-right: 0;
|
||||
padding-inline-end: 0;
|
||||
padding-inline-start: initial;
|
||||
@@ -661,18 +629,18 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
/* ============= VERY VERY NARROW ============= */
|
||||
|
||||
:host([veryVeryNarrow]) .info {
|
||||
[class*="very-very-narrow"] .info {
|
||||
padding-top: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host([veryVeryNarrow]) .content {
|
||||
[class*="very-very-narrow"] .content {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host([veryVeryNarrow]) .icon-image {
|
||||
[class*="very-very-narrow"] .icon-image {
|
||||
margin-right: 0;
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: initial;
|
||||
|
@@ -16,6 +16,7 @@ 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";
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
getCustomCardEntry,
|
||||
} from "../../../../data/lovelace_custom_cards";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getStripDiacriticsFn } from "../../../../util/fuse";
|
||||
import {
|
||||
calcUnusedEntities,
|
||||
computeUsedEntities,
|
||||
@@ -86,9 +88,10 @@ 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(filter).map((result) => result.item);
|
||||
cards = fuse.search(stripDiacritics(filter)).map((result) => result.item);
|
||||
return cardElements.filter((cardElement: CardElement) =>
|
||||
cards.includes(cardElement.card)
|
||||
);
|
||||
|
@@ -419,11 +419,6 @@
|
||||
"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"
|
||||
@@ -535,7 +530,8 @@
|
||||
"selected": "Selected {selected}",
|
||||
"close_select_mode": "Close selection mode",
|
||||
"select_all": "Select all",
|
||||
"select_none": "Select none"
|
||||
"select_none": "Select none",
|
||||
"settings": "Customize table"
|
||||
},
|
||||
"config-entry-picker": {
|
||||
"config_entry": "Integration"
|
||||
@@ -804,7 +800,14 @@
|
||||
"filtering_by": "Filtering by",
|
||||
"hidden": "{number} hidden",
|
||||
"clear": "Clear",
|
||||
"ungrouped": "Ungrouped"
|
||||
"ungrouped": "Ungrouped",
|
||||
"settings": {
|
||||
"header": "Customize",
|
||||
"hide": "Hide column {title}",
|
||||
"show": "Show column {title}",
|
||||
"done": "Done",
|
||||
"restore": "Restore defaults"
|
||||
}
|
||||
},
|
||||
"media-browser": {
|
||||
"tts": {
|
||||
@@ -2076,6 +2079,7 @@
|
||||
"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%]",
|
||||
@@ -2670,6 +2674,7 @@
|
||||
"caption": "Expose",
|
||||
"headers": {
|
||||
"name": "Name",
|
||||
"entity_id": "Entity ID",
|
||||
"area": "Area",
|
||||
"domain": "Domain",
|
||||
"assistants": "Assistants",
|
||||
@@ -2764,6 +2769,12 @@
|
||||
"unavailable": "Automation is unavailable",
|
||||
"migrate": "Migrate",
|
||||
"duplicate": "[%key:ui::common::duplicate%]",
|
||||
"take_control": "Take control",
|
||||
"take_control_confirmation": {
|
||||
"title": "Take control of automation?",
|
||||
"text": "This automation is using a blueprint. By taking control, your automation will be converted into a regular automation using triggers, conditions and actions. You will be able to edit it directly and you won't be able to convert it back to a blueprint.",
|
||||
"action": "Take control"
|
||||
},
|
||||
"run": "[%key:ui::panel::config::automation::editor::actions::run%]",
|
||||
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
|
||||
"show_trace": "Traces",
|
||||
@@ -3634,6 +3645,12 @@
|
||||
"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%]",
|
||||
"take_control_confirmation": {
|
||||
"title": "Take control of script?",
|
||||
"text": "This script is using a blueprint. By taking control, your script will be converted into a regular automation using actions. You will be able to edit it directly and you won't be able to convert it back to a blueprint.",
|
||||
"action": "[%key:ui::panel::config::automation::editor::take_control_confirmation::action%]"
|
||||
},
|
||||
"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",
|
||||
@@ -4033,6 +4050,7 @@
|
||||
"update_device_error": "Updating the device failed",
|
||||
"disabled": "Disabled",
|
||||
"data_table": {
|
||||
"icon": "Icon",
|
||||
"device": "Device",
|
||||
"manufacturer": "Manufacturer",
|
||||
"model": "Model",
|
||||
@@ -4143,10 +4161,18 @@
|
||||
"delete": "Delete",
|
||||
"create": "Create",
|
||||
"update": "Update",
|
||||
"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.",
|
||||
"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%]",
|
||||
"admin": "[%key:ui::panel::config::users::editor::admin%]",
|
||||
"local_only": "[%key:ui::panel::config::users::editor::local_only%]",
|
||||
"allow_login": "Allow person to login"
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
@@ -4253,9 +4279,9 @@
|
||||
"config_entry": {
|
||||
"application_credentials": {
|
||||
"delete_title": "Application credentials",
|
||||
"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",
|
||||
"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",
|
||||
"dismiss": "Keep",
|
||||
"learn_more": "Learn more about application credentials"
|
||||
},
|
||||
@@ -4386,6 +4412,7 @@
|
||||
"caption": "View user",
|
||||
"name": "Display name",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"change_password": "Change password",
|
||||
"change_username": "Change username",
|
||||
"activate_user": "Activate user",
|
||||
@@ -4395,16 +4422,17 @@
|
||||
"id": "ID",
|
||||
"owner": "Owner",
|
||||
"admin": "Administrator",
|
||||
"admin_description": "Administrators can manage users, devices, automations and dashboards.",
|
||||
"group": "Group",
|
||||
"active": "Active",
|
||||
"local_only": "Can only log in from the local network",
|
||||
"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",
|
||||
"system_generated": "System user",
|
||||
"system_generated_users_not_removable": "Unable to remove system users.",
|
||||
"system_generated_users_not_editable": "Unable to update system users.",
|
||||
"system_generated_read_only_users": "System users can not be updated.",
|
||||
"unnamed_user": "Unnamed User",
|
||||
"confirm_user_deletion_title": "Delete {name}?",
|
||||
"confirm_user_deletion_text": "This user will be permanently deleted.",
|
||||
"active_tooltip": "Controls if user can login"
|
||||
"confirm_user_deletion_text": "This user will be permanently deleted."
|
||||
},
|
||||
"add_user": {
|
||||
"caption": "Add user",
|
||||
@@ -4454,11 +4482,15 @@
|
||||
"client_id": "OAuth client ID",
|
||||
"application": "Integration"
|
||||
},
|
||||
"remove": {
|
||||
"button": "Delete application credential",
|
||||
"confirm_title": "Delete application credential?"
|
||||
},
|
||||
"remove_selected": {
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"selected": "{number} selected"
|
||||
}
|
||||
@@ -5061,6 +5093,11 @@
|
||||
"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": {
|
||||
@@ -6864,7 +6901,7 @@
|
||||
"forums": "Home Assistant forums",
|
||||
"open_home_newsletter": "Building the Open Home newsletter",
|
||||
"discord": "Discord chat",
|
||||
"twitter": "Twitter",
|
||||
"x": "[%key:ui::panel::config::tips::join_x%]",
|
||||
"playstore": "Get it on Google Play",
|
||||
"appstore": "Download on the App Store"
|
||||
},
|
||||
|
12
src/util/fuse.ts
Normal file
12
src/util/fuse.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
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) ?? "");
|
||||
};
|
28
yarn.lock
28
yarn.lock
@@ -4356,10 +4356,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mocha@npm:10.0.6":
|
||||
version: 10.0.6
|
||||
resolution: "@types/mocha@npm:10.0.6"
|
||||
checksum: 10/fc73626e81e89c32d06b7ff9b72c4177b46d579cdd932f796614adc026852d84cb849d743473ba572cb4d9ea6d8c04e3749552d326c26495ec1c4b46e6e0a0c0
|
||||
"@types/mocha@npm:10.0.7":
|
||||
version: 10.0.7
|
||||
resolution: "@types/mocha@npm:10.0.7"
|
||||
checksum: 10/4494871e8a867633d818b00d6f29d47379f9e23655b89ca728166ff2f0a406b97d376fcc3e7a570a3840f72abb03c886c5e66f50ae0f018376e4dc10ed179564
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8996,7 +8996,7 @@ __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.6"
|
||||
"@types/mocha": "npm:10.0.7"
|
||||
"@types/qrcode": "npm:1.5.5"
|
||||
"@types/serve-handler": "npm:6.1.4"
|
||||
"@types/sortablejs": "npm:1.15.8"
|
||||
@@ -9100,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.4.5"
|
||||
typescript: "npm:5.5.2"
|
||||
ua-parser-js: "npm:1.0.38"
|
||||
unfetch: "npm:5.0.0"
|
||||
vis-data: "npm:7.1.9"
|
||||
@@ -14226,13 +14226,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript@npm:5.4.5":
|
||||
version: 5.4.5
|
||||
resolution: "typescript@npm:5.4.5"
|
||||
"typescript@npm:5.5.2":
|
||||
version: 5.5.2
|
||||
resolution: "typescript@npm:5.5.2"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 10/d04a9e27e6d83861f2126665aa8d84847e8ebabcea9125b9ebc30370b98cb38b5dff2508d74e2326a744938191a83a69aa9fddab41f193ffa43eabfdf3f190a5
|
||||
checksum: 10/9118b20f248e76b0dbff8737fef65dfa89d02668d4e633d2c5ceac99033a0ca5e8a1c1a53bc94da68e8f67677a88f318663dde859c9e9a09c1e116415daec2ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14246,13 +14246,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"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"
|
||||
"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"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 10/760f7d92fb383dbf7dee2443bf902f4365db2117f96f875cf809167f6103d55064de973db9f78fe8f31ec08fff52b2c969aee0d310939c0a3798ec75d0bca2e1
|
||||
checksum: 10/ac3145f65cf9e72ab29f2196e05d5816b355dc1a9195b9f010d285182a12457cfacd068be2dd22c877f88ebc966ac6e0e83f51c8586412b16499a27e3670ff4b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user