mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-15 13:26:34 +00:00
Add categories, filtering, grouping to automation panel (#20197)
* Add categories and filtering to automation panel * Update search-input-outlined.ts * Update ha-config-entities.ts * fix resetting area filter * fixes * Update ha-category-picker.ts * Update ha-filter-blueprints.ts * fix updating badge * fix overflow issue
This commit is contained in:
parent
141c8c5192
commit
68935d46ce
@ -73,6 +73,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
name: null,
|
name: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
labels: [],
|
labels: [],
|
||||||
|
categories: {},
|
||||||
platform: "co2signal",
|
platform: "co2signal",
|
||||||
hidden_by: null,
|
hidden_by: null,
|
||||||
entity_category: null,
|
entity_category: null,
|
||||||
@ -90,6 +91,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
name: null,
|
name: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
labels: [],
|
labels: [],
|
||||||
|
categories: {},
|
||||||
platform: "co2signal",
|
platform: "co2signal",
|
||||||
hidden_by: null,
|
hidden_by: null,
|
||||||
entity_category: null,
|
entity_category: null,
|
||||||
|
@ -200,6 +200,7 @@ const createEntityRegistryEntries = (
|
|||||||
unique_id: "updater",
|
unique_id: "updater",
|
||||||
options: null,
|
options: null,
|
||||||
labels: [],
|
labels: [],
|
||||||
|
categories: {},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -4,22 +4,32 @@ import { css, html } from "lit";
|
|||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-assist-chip")
|
@customElement("ha-assist-chip")
|
||||||
|
// @ts-ignore
|
||||||
export class HaAssistChip extends MdAssistChip {
|
export class HaAssistChip extends MdAssistChip {
|
||||||
@property({ type: Boolean, reflect: true }) filled = false;
|
@property({ type: Boolean, reflect: true }) filled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) active = false;
|
||||||
|
|
||||||
static override styles = [
|
static override styles = [
|
||||||
...super.styles,
|
...super.styles,
|
||||||
css`
|
css`
|
||||||
:host {
|
:host {
|
||||||
--md-sys-color-primary: var(--primary-text-color);
|
--md-sys-color-primary: var(--primary-text-color);
|
||||||
--md-sys-color-on-surface: var(--primary-text-color);
|
--md-sys-color-on-surface: var(--primary-text-color);
|
||||||
--md-assist-chip-container-shape: 16px;
|
--md-assist-chip-container-shape: var(
|
||||||
|
--ha-assist-chip-container-shape,
|
||||||
|
16px
|
||||||
|
);
|
||||||
--md-assist-chip-outline-color: var(--outline-color);
|
--md-assist-chip-outline-color: var(--outline-color);
|
||||||
--md-assist-chip-label-text-weight: 400;
|
--md-assist-chip-label-text-weight: 400;
|
||||||
--ha-assist-chip-filled-container-color: rgba(
|
--ha-assist-chip-filled-container-color: rgba(
|
||||||
var(--rgb-primary-text-color),
|
var(--rgb-primary-text-color),
|
||||||
0.15
|
0.15
|
||||||
);
|
);
|
||||||
|
--ha-assist-chip-active-container-color: rgba(
|
||||||
|
var(--rgb-primary-color),
|
||||||
|
0.15
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/** Material 3 doesn't have a filled chip, so we have to make our own **/
|
/** Material 3 doesn't have a filled chip, so we have to make our own **/
|
||||||
.filled {
|
.filled {
|
||||||
@ -31,10 +41,21 @@ export class HaAssistChip extends MdAssistChip {
|
|||||||
background-color: var(--ha-assist-chip-filled-container-color);
|
background-color: var(--ha-assist-chip-filled-container-color);
|
||||||
}
|
}
|
||||||
/** Set the size of mdc icons **/
|
/** Set the size of mdc icons **/
|
||||||
::slotted([slot="icon"]) {
|
::slotted([slot="icon"]),
|
||||||
|
::slotted([slot="trailingIcon"]) {
|
||||||
display: flex;
|
display: flex;
|
||||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trailing.icon ::slotted(*),
|
||||||
|
.trailing.icon svg {
|
||||||
|
margin-inline-end: unset;
|
||||||
|
margin-inline-start: var(--_icon-label-space);
|
||||||
|
}
|
||||||
|
:where(.active)::before {
|
||||||
|
background: var(--ha-assist-chip-active-container-color);
|
||||||
|
opacity: var(--ha-assist-chip-active-container-opacity);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -45,6 +66,30 @@ export class HaAssistChip extends MdAssistChip {
|
|||||||
|
|
||||||
return super.renderOutline();
|
return super.renderOutline();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override getContainerClasses() {
|
||||||
|
return {
|
||||||
|
...super.getContainerClasses(),
|
||||||
|
active: this.active,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override renderPrimaryContent() {
|
||||||
|
return html`
|
||||||
|
<span class="leading icon" aria-hidden="true">
|
||||||
|
${this.renderLeadingIcon()}
|
||||||
|
</span>
|
||||||
|
<span class="label">${this.label}</span>
|
||||||
|
<span class="touch"></span>
|
||||||
|
<span class="trailing leading icon" aria-hidden="true">
|
||||||
|
${this.renderTrailingIcon()}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderTrailingIcon() {
|
||||||
|
return html`<slot name="trailing-icon"></slot>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
118
src/components/data-table/ha-data-table-labels.ts
Normal file
118
src/components/data-table/ha-data-table-labels.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { css, html, LitElement, nothing, TemplateResult } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import "../chips/ha-assist-chip";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import { LabelRegistryEntry } from "../../data/label_registry";
|
||||||
|
import { computeCssColor } from "../../common/color/compute-color";
|
||||||
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
|
||||||
|
@customElement("ha-data-table-labels")
|
||||||
|
class HaDataTableLabels extends LitElement {
|
||||||
|
@property({ attribute: false }) public labels!: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-chip-set>
|
||||||
|
${repeat(
|
||||||
|
this.labels.slice(0, 2),
|
||||||
|
(label) => label.label_id,
|
||||||
|
(label) => this._renderLabel(label, true)
|
||||||
|
)}
|
||||||
|
${this.labels.length > 2
|
||||||
|
? html`<ha-button-menu
|
||||||
|
absolute
|
||||||
|
@click=${this._handleIconOverflowMenuOpened}
|
||||||
|
@closed=${this._handleIconOverflowMenuClosed}
|
||||||
|
>
|
||||||
|
<ha-assist-chip
|
||||||
|
slot="trigger"
|
||||||
|
.label=${`+${this.labels.length - 2}`}
|
||||||
|
></ha-assist-chip>
|
||||||
|
${repeat(
|
||||||
|
this.labels.slice(2),
|
||||||
|
(label) => label.label_id,
|
||||||
|
(label) =>
|
||||||
|
html`<ha-list-item
|
||||||
|
@click=${this._labelClicked}
|
||||||
|
.item=${label}
|
||||||
|
>
|
||||||
|
${this._renderLabel(label, false)}
|
||||||
|
</ha-list-item>`
|
||||||
|
)}
|
||||||
|
</ha-button-menu>`
|
||||||
|
: nothing}
|
||||||
|
</ha-chip-set>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderLabel(label: LabelRegistryEntry, clickAction: boolean) {
|
||||||
|
const color = label?.color ? computeCssColor(label.color) : undefined;
|
||||||
|
return html`<ha-assist-chip
|
||||||
|
.item=${label}
|
||||||
|
@click=${clickAction ? this._labelClicked : undefined}
|
||||||
|
.label=${label?.name}
|
||||||
|
active
|
||||||
|
style=${color ? `--color: ${color}` : ""}
|
||||||
|
>
|
||||||
|
${label?.icon
|
||||||
|
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
</ha-assist-chip>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _labelClicked(ev: Event) {
|
||||||
|
const label = (ev.currentTarget as any).item as LabelRegistryEntry;
|
||||||
|
fireEvent(this, "label-clicked", { label });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _handleIconOverflowMenuOpened(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
// If this component is used inside a data table, the z-index of the row
|
||||||
|
// needs to be increased. Otherwise the ha-button-menu would be displayed
|
||||||
|
// underneath the next row in the table.
|
||||||
|
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||||
|
if (row) {
|
||||||
|
row.style.zIndex = "1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _handleIconOverflowMenuClosed() {
|
||||||
|
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||||
|
if (row) {
|
||||||
|
row.style.zIndex = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-top: 4px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
ha-chip-set {
|
||||||
|
position: fixed;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
ha-assist-chip {
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
--md-assist-chip-icon-size: 16px;
|
||||||
|
--md-assist-chip-container-height: 20px;
|
||||||
|
--md-assist-chip-leading-space: 12px;
|
||||||
|
--md-assist-chip-trailing-space: 12px;
|
||||||
|
--ha-assist-chip-active-container-color: var(--color);
|
||||||
|
--ha-assist-chip-active-container-opacity: 0.3;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-data-table-labels": HaDataTableLabels;
|
||||||
|
}
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"label-clicked": { label: LabelRegistryEntry };
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,7 @@ import type { HaCheckbox } from "../ha-checkbox";
|
|||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "../search-input";
|
import "../search-input";
|
||||||
import { filterData, sortData } from "./sort-filter";
|
import { filterData, sortData } from "./sort-filter";
|
||||||
|
import { groupBy } from "../../common/util/group-by";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
@ -67,13 +68,20 @@ export interface DataTableSortColumnData {
|
|||||||
filterKey?: string;
|
filterKey?: string;
|
||||||
valueColumn?: string;
|
valueColumn?: string;
|
||||||
direction?: SortingDirection;
|
direction?: SortingDirection;
|
||||||
|
groupable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||||
main?: boolean;
|
main?: boolean;
|
||||||
title: TemplateResult | string;
|
title: TemplateResult | string;
|
||||||
label?: TemplateResult | string;
|
label?: TemplateResult | string;
|
||||||
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
|
type?:
|
||||||
|
| "numeric"
|
||||||
|
| "icon"
|
||||||
|
| "icon-button"
|
||||||
|
| "overflow"
|
||||||
|
| "overflow-menu"
|
||||||
|
| "flex";
|
||||||
template?: (row: T) => TemplateResult | string | typeof nothing;
|
template?: (row: T) => TemplateResult | string | typeof nothing;
|
||||||
width?: string;
|
width?: string;
|
||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
@ -95,6 +103,8 @@ export interface SortableColumnContainer {
|
|||||||
[key: string]: ClonedDataTableColumnData;
|
[key: string]: ClonedDataTableColumnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||||
|
|
||||||
@customElement("ha-data-table")
|
@customElement("ha-data-table")
|
||||||
export class HaDataTable extends LitElement {
|
export class HaDataTable extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -129,14 +139,16 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
@property({ type: String }) public filter = "";
|
@property({ type: String }) public filter = "";
|
||||||
|
|
||||||
|
@property() public groupColumn?: string;
|
||||||
|
|
||||||
|
@property() public sortColumn?: string;
|
||||||
|
|
||||||
|
@property() public sortDirection: SortingDirection = null;
|
||||||
|
|
||||||
@state() private _filterable = false;
|
@state() private _filterable = false;
|
||||||
|
|
||||||
@state() private _filter = "";
|
@state() private _filter = "";
|
||||||
|
|
||||||
@state() private _sortColumn?: string;
|
|
||||||
|
|
||||||
@state() private _sortDirection: SortingDirection = null;
|
|
||||||
|
|
||||||
@state() private _filteredData: DataTableRowData[] = [];
|
@state() private _filteredData: DataTableRowData[] = [];
|
||||||
|
|
||||||
@state() private _headerHeight = 0;
|
@state() private _headerHeight = 0;
|
||||||
@ -195,8 +207,14 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
for (const columnId in this.columns) {
|
for (const columnId in this.columns) {
|
||||||
if (this.columns[columnId].direction) {
|
if (this.columns[columnId].direction) {
|
||||||
this._sortDirection = this.columns[columnId].direction!;
|
this.sortDirection = this.columns[columnId].direction!;
|
||||||
this._sortColumn = columnId;
|
this.sortColumn = columnId;
|
||||||
|
|
||||||
|
fireEvent(this, "sorting-changed", {
|
||||||
|
column: columnId,
|
||||||
|
direction: this.sortDirection,
|
||||||
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,11 +244,16 @@ export class HaDataTable extends LitElement {
|
|||||||
properties.has("data") ||
|
properties.has("data") ||
|
||||||
properties.has("columns") ||
|
properties.has("columns") ||
|
||||||
properties.has("_filter") ||
|
properties.has("_filter") ||
|
||||||
properties.has("_sortColumn") ||
|
properties.has("sortColumn") ||
|
||||||
properties.has("_sortDirection")
|
properties.has("sortDirection") ||
|
||||||
|
properties.has("groupColumn")
|
||||||
) {
|
) {
|
||||||
this._sortFilterData();
|
this._sortFilterData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (properties.has("selectable")) {
|
||||||
|
this._items = [...this._items];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@ -263,75 +286,79 @@ export class HaDataTable extends LitElement {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="mdc-data-table__header-row" role="row" aria-rowindex="1">
|
<div class="mdc-data-table__header-row" role="row" aria-rowindex="1">
|
||||||
${this.selectable
|
<slot name="header-row">
|
||||||
? html`
|
${this.selectable
|
||||||
<div
|
? html`
|
||||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
<div
|
||||||
role="columnheader"
|
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||||
>
|
role="columnheader"
|
||||||
<ha-checkbox
|
|
||||||
class="mdc-data-table__row-checkbox"
|
|
||||||
@change=${this._handleHeaderRowCheckboxClick}
|
|
||||||
.indeterminate=${this._checkedRows.length &&
|
|
||||||
this._checkedRows.length !== this._checkableRowsCount}
|
|
||||||
.checked=${this._checkedRows.length &&
|
|
||||||
this._checkedRows.length === this._checkableRowsCount}
|
|
||||||
>
|
>
|
||||||
</ha-checkbox>
|
<ha-checkbox
|
||||||
|
class="mdc-data-table__row-checkbox"
|
||||||
|
@change=${this._handleHeaderRowCheckboxClick}
|
||||||
|
.indeterminate=${this._checkedRows.length &&
|
||||||
|
this._checkedRows.length !== this._checkableRowsCount}
|
||||||
|
.checked=${this._checkedRows.length &&
|
||||||
|
this._checkedRows.length === this._checkableRowsCount}
|
||||||
|
>
|
||||||
|
</ha-checkbox>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
${Object.entries(this.columns).map(([key, column]) => {
|
||||||
|
if (column.hidden) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const sorted = key === this.sortColumn;
|
||||||
|
const classes = {
|
||||||
|
"mdc-data-table__header-cell--numeric":
|
||||||
|
column.type === "numeric",
|
||||||
|
"mdc-data-table__header-cell--icon": column.type === "icon",
|
||||||
|
"mdc-data-table__header-cell--icon-button":
|
||||||
|
column.type === "icon-button",
|
||||||
|
"mdc-data-table__header-cell--overflow-menu":
|
||||||
|
column.type === "overflow-menu",
|
||||||
|
"mdc-data-table__header-cell--overflow":
|
||||||
|
column.type === "overflow",
|
||||||
|
sortable: Boolean(column.sortable),
|
||||||
|
"not-sorted": Boolean(column.sortable && !sorted),
|
||||||
|
grows: Boolean(column.grows),
|
||||||
|
};
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
aria-label=${ifDefined(column.label)}
|
||||||
|
class="mdc-data-table__header-cell ${classMap(classes)}"
|
||||||
|
style=${column.width
|
||||||
|
? styleMap({
|
||||||
|
[column.grows ? "minWidth" : "width"]: column.width,
|
||||||
|
maxWidth: column.maxWidth || "",
|
||||||
|
})
|
||||||
|
: ""}
|
||||||
|
role="columnheader"
|
||||||
|
aria-sort=${ifDefined(
|
||||||
|
sorted
|
||||||
|
? this.sortDirection === "desc"
|
||||||
|
? "descending"
|
||||||
|
: "ascending"
|
||||||
|
: undefined
|
||||||
|
)}
|
||||||
|
@click=${this._handleHeaderClick}
|
||||||
|
.columnId=${key}
|
||||||
|
>
|
||||||
|
${column.sortable
|
||||||
|
? html`
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${sorted && this.sortDirection === "desc"
|
||||||
|
? mdiArrowDown
|
||||||
|
: mdiArrowUp}
|
||||||
|
></ha-svg-icon>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<span>${column.title}</span>
|
||||||
</div>
|
</div>
|
||||||
`
|
`;
|
||||||
: ""}
|
})}
|
||||||
${Object.entries(this.columns).map(([key, column]) => {
|
</slot>
|
||||||
if (column.hidden) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const sorted = key === this._sortColumn;
|
|
||||||
const classes = {
|
|
||||||
"mdc-data-table__header-cell--numeric":
|
|
||||||
column.type === "numeric",
|
|
||||||
"mdc-data-table__header-cell--icon": column.type === "icon",
|
|
||||||
"mdc-data-table__header-cell--icon-button":
|
|
||||||
column.type === "icon-button",
|
|
||||||
"mdc-data-table__header-cell--overflow-menu":
|
|
||||||
column.type === "overflow-menu",
|
|
||||||
sortable: Boolean(column.sortable),
|
|
||||||
"not-sorted": Boolean(column.sortable && !sorted),
|
|
||||||
grows: Boolean(column.grows),
|
|
||||||
};
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
aria-label=${ifDefined(column.label)}
|
|
||||||
class="mdc-data-table__header-cell ${classMap(classes)}"
|
|
||||||
style=${column.width
|
|
||||||
? styleMap({
|
|
||||||
[column.grows ? "minWidth" : "width"]: column.width,
|
|
||||||
maxWidth: column.maxWidth || "",
|
|
||||||
})
|
|
||||||
: ""}
|
|
||||||
role="columnheader"
|
|
||||||
aria-sort=${ifDefined(
|
|
||||||
sorted
|
|
||||||
? this._sortDirection === "desc"
|
|
||||||
? "descending"
|
|
||||||
: "ascending"
|
|
||||||
: undefined
|
|
||||||
)}
|
|
||||||
@click=${this._handleHeaderClick}
|
|
||||||
.columnId=${key}
|
|
||||||
>
|
|
||||||
${column.sortable
|
|
||||||
? html`
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${sorted && this._sortDirection === "desc"
|
|
||||||
? mdiArrowDown
|
|
||||||
: mdiArrowUp}
|
|
||||||
></ha-svg-icon>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<span>${column.title}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
${!this._filteredData.length
|
${!this._filteredData.length
|
||||||
? html`
|
? html`
|
||||||
@ -408,7 +435,7 @@ export class HaDataTable extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
${Object.entries(this.columns).map(([key, column]) => {
|
${Object.entries(this.columns).map(([key, column]) => {
|
||||||
if (column.hidden) {
|
if (column.hidden) {
|
||||||
return "";
|
return nothing;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@ -421,6 +448,7 @@ export class HaDataTable extends LitElement {
|
|||||||
column.type === "icon-button",
|
column.type === "icon-button",
|
||||||
"mdc-data-table__cell--overflow-menu":
|
"mdc-data-table__cell--overflow-menu":
|
||||||
column.type === "overflow-menu",
|
column.type === "overflow-menu",
|
||||||
|
"mdc-data-table__cell--overflow": column.type === "overflow",
|
||||||
grows: Boolean(column.grows),
|
grows: Boolean(column.grows),
|
||||||
forceLTR: Boolean(column.forceLTR),
|
forceLTR: Boolean(column.forceLTR),
|
||||||
})}"
|
})}"
|
||||||
@ -453,12 +481,12 @@ export class HaDataTable extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prom = this._sortColumn
|
const prom = this.sortColumn
|
||||||
? sortData(
|
? sortData(
|
||||||
filteredData,
|
filteredData,
|
||||||
this._sortColumns[this._sortColumn],
|
this._sortColumns[this.sortColumn],
|
||||||
this._sortDirection,
|
this.sortDirection,
|
||||||
this._sortColumn,
|
this.sortColumn,
|
||||||
this.hass.locale.language
|
this.hass.locale.language
|
||||||
)
|
)
|
||||||
: filteredData;
|
: filteredData;
|
||||||
@ -477,7 +505,7 @@ export class HaDataTable extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.appendRow || this.hasFab) {
|
if (this.appendRow || this.hasFab || this.groupColumn) {
|
||||||
const items = [...data];
|
const items = [...data];
|
||||||
|
|
||||||
if (this.appendRow) {
|
if (this.appendRow) {
|
||||||
@ -487,7 +515,41 @@ export class HaDataTable extends LitElement {
|
|||||||
if (this.hasFab) {
|
if (this.hasFab) {
|
||||||
items.push({ empty: true });
|
items.push({ empty: true });
|
||||||
}
|
}
|
||||||
this._items = items;
|
|
||||||
|
if (this.groupColumn) {
|
||||||
|
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
|
||||||
|
if (grouped.undefined) {
|
||||||
|
// make sure ungrouped items are at the bottom
|
||||||
|
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
||||||
|
delete grouped.undefined;
|
||||||
|
}
|
||||||
|
const sorted: {
|
||||||
|
[key: string]: DataTableRowData[];
|
||||||
|
} = Object.keys(grouped)
|
||||||
|
.sort()
|
||||||
|
.reduce((obj, key) => {
|
||||||
|
obj[key] = grouped[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
const groupedItems: DataTableRowData[] = [];
|
||||||
|
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||||
|
groupedItems.push({
|
||||||
|
append: true,
|
||||||
|
content: html`<div
|
||||||
|
class="mdc-data-table__cell group-header"
|
||||||
|
role="cell"
|
||||||
|
>
|
||||||
|
${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""}
|
||||||
|
</div>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
groupedItems.push(...rows);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._items = groupedItems;
|
||||||
|
} else {
|
||||||
|
this._items = items;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this._items = data;
|
this._items = data;
|
||||||
}
|
}
|
||||||
@ -507,19 +569,19 @@ export class HaDataTable extends LitElement {
|
|||||||
if (!this.columns[columnId].sortable) {
|
if (!this.columns[columnId].sortable) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this._sortDirection || this._sortColumn !== columnId) {
|
if (!this.sortDirection || this.sortColumn !== columnId) {
|
||||||
this._sortDirection = "asc";
|
this.sortDirection = "asc";
|
||||||
} else if (this._sortDirection === "asc") {
|
} else if (this.sortDirection === "asc") {
|
||||||
this._sortDirection = "desc";
|
this.sortDirection = "desc";
|
||||||
} else {
|
} else {
|
||||||
this._sortDirection = null;
|
this.sortDirection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._sortColumn = this._sortDirection === null ? undefined : columnId;
|
this.sortColumn = this.sortDirection === null ? undefined : columnId;
|
||||||
|
|
||||||
fireEvent(this, "sorting-changed", {
|
fireEvent(this, "sorting-changed", {
|
||||||
column: columnId,
|
column: columnId,
|
||||||
direction: this._sortDirection,
|
direction: this.sortDirection,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -552,8 +614,15 @@ export class HaDataTable extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _handleRowClick = (ev: Event) => {
|
private _handleRowClick = (ev: Event) => {
|
||||||
const target = ev.target as HTMLElement;
|
if (
|
||||||
if (["HA-CHECKBOX", "MWC-BUTTON"].includes(target.tagName)) {
|
ev
|
||||||
|
.composedPath()
|
||||||
|
.find((el) =>
|
||||||
|
["ha-checkbox", "mwc-button", "ha-button", "ha-assist-chip"].includes(
|
||||||
|
(el as HTMLElement).localName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rowId = (ev.currentTarget as any).rowId;
|
const rowId = (ev.currentTarget as any).rowId;
|
||||||
@ -629,7 +698,7 @@ export class HaDataTable extends LitElement {
|
|||||||
.mdc-data-table__row {
|
.mdc-data-table__row {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 52px;
|
height: var(--data-table-row-height, 52px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||||
@ -655,7 +724,6 @@ export class HaDataTable extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
overflow-x: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mdc-data-table__header-row::-webkit-scrollbar {
|
.mdc-data-table__header-row::-webkit-scrollbar {
|
||||||
@ -809,7 +877,9 @@ export class HaDataTable extends LitElement {
|
|||||||
padding-inline-start: initial;
|
padding-inline-start: initial;
|
||||||
}
|
}
|
||||||
.mdc-data-table__cell--overflow-menu,
|
.mdc-data-table__cell--overflow-menu,
|
||||||
.mdc-data-table__header-cell--overflow-menu {
|
.mdc-data-table__cell--overflow,
|
||||||
|
.mdc-data-table__header-cell--overflow-menu,
|
||||||
|
.mdc-data-table__header-cell--overflow {
|
||||||
overflow: initial;
|
overflow: initial;
|
||||||
}
|
}
|
||||||
.mdc-data-table__cell--icon-button a {
|
.mdc-data-table__cell--icon-button a {
|
||||||
@ -839,6 +909,12 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
/* custom from here */
|
/* custom from here */
|
||||||
|
|
||||||
|
.group-header {
|
||||||
|
padding-top: 12px;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
@ -39,25 +39,21 @@ interface FloorAreaEntry {
|
|||||||
icon: string | null;
|
icon: string | null;
|
||||||
strings: string[];
|
strings: string[];
|
||||||
type: "floor" | "area";
|
type: "floor" | "area";
|
||||||
|
hasFloor?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) =>
|
const rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) =>
|
||||||
item.type === "floor"
|
html`<ha-list-item
|
||||||
? html`<ha-list-item graphic="icon" class="floor">
|
graphic="icon"
|
||||||
${item.icon
|
style=${item.type === "area" && item.hasFloor
|
||||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
? "--mdc-list-side-padding-left: 48px;"
|
||||||
: nothing}
|
: ""}
|
||||||
${item.name}
|
>
|
||||||
</ha-list-item>`
|
${item.icon
|
||||||
: html`<ha-list-item
|
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||||
graphic="icon"
|
: nothing}
|
||||||
style="--mdc-list-side-padding-left: 48px;"
|
${item.name}
|
||||||
>
|
</ha-list-item>`;
|
||||||
${item.icon
|
|
||||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
|
||||||
: nothing}
|
|
||||||
${item.name}
|
|
||||||
</ha-list-item>`;
|
|
||||||
|
|
||||||
@customElement("ha-area-floor-picker")
|
@customElement("ha-area-floor-picker")
|
||||||
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
||||||
@ -363,6 +359,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
name: area.name,
|
name: area.name,
|
||||||
icon: area.icon,
|
icon: area.icon,
|
||||||
strings: [area.area_id, ...area.aliases, area.name],
|
strings: [area.area_id, ...area.aliases, area.name],
|
||||||
|
hasFloor: true,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,221 +0,0 @@
|
|||||||
import type { Corner } from "@material/mwc-menu";
|
|
||||||
import "@material/mwc-menu/mwc-menu-surface";
|
|
||||||
import { mdiFilterVariant } from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
|
||||||
import { computeDeviceName } from "../data/device_registry";
|
|
||||||
import { findRelated, RelatedResult } from "../data/search";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./device/ha-device-picker";
|
|
||||||
import "./entity/ha-entity-picker";
|
|
||||||
import "./ha-area-picker";
|
|
||||||
import "./ha-icon-button";
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
// for fire event
|
|
||||||
interface HASSDomEvents {
|
|
||||||
"related-changed": {
|
|
||||||
value?: FilterValue;
|
|
||||||
items?: RelatedResult;
|
|
||||||
filter?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FilterValue {
|
|
||||||
area?: string;
|
|
||||||
device?: string;
|
|
||||||
entity?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement("ha-button-related-filter-menu")
|
|
||||||
export class HaRelatedFilterButtonMenu extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public corner: Corner = "BOTTOM_START";
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: FilterValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show no entities of these domains.
|
|
||||||
* @type {Array}
|
|
||||||
* @attr exclude-domains
|
|
||||||
*/
|
|
||||||
@property({ type: Array, attribute: "exclude-domains" })
|
|
||||||
public excludeDomains?: string[];
|
|
||||||
|
|
||||||
@state() private _open = false;
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<ha-icon-button
|
|
||||||
@click=${this._handleClick}
|
|
||||||
.label=${this.hass.localize("ui.components.related-filter-menu.filter")}
|
|
||||||
.path=${mdiFilterVariant}
|
|
||||||
></ha-icon-button>
|
|
||||||
<mwc-menu-surface
|
|
||||||
.open=${this._open}
|
|
||||||
.anchor=${this}
|
|
||||||
.fullwidth=${this.narrow}
|
|
||||||
.corner=${this.corner}
|
|
||||||
@closed=${this._onClosed}
|
|
||||||
@input=${stopPropagation}
|
|
||||||
>
|
|
||||||
<ha-area-picker
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filter_by_area"
|
|
||||||
)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this.value?.area}
|
|
||||||
no-add
|
|
||||||
@value-changed=${this._areaPicked}
|
|
||||||
@click=${this._preventDefault}
|
|
||||||
></ha-area-picker>
|
|
||||||
<ha-device-picker
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filter_by_device"
|
|
||||||
)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this.value?.device}
|
|
||||||
@value-changed=${this._devicePicked}
|
|
||||||
@click=${this._preventDefault}
|
|
||||||
></ha-device-picker>
|
|
||||||
<ha-entity-picker
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filter_by_entity"
|
|
||||||
)}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this.value?.entity}
|
|
||||||
.excludeDomains=${this.excludeDomains}
|
|
||||||
@value-changed=${this._entityPicked}
|
|
||||||
@click=${this._preventDefault}
|
|
||||||
></ha-entity-picker>
|
|
||||||
</mwc-menu-surface>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleClick(): void {
|
|
||||||
if (this.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._open = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onClosed(ev): void {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _preventDefault(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _entityPicked(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const entityId = ev.detail.value;
|
|
||||||
if (!entityId) {
|
|
||||||
fireEvent(this, "related-changed", { value: undefined });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const filter = this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filtered_by_entity",
|
|
||||||
{
|
|
||||||
entity_name: computeStateName(
|
|
||||||
(ev.currentTarget as any).comboBox.selectedItem
|
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const items = await findRelated(this.hass, "entity", entityId);
|
|
||||||
fireEvent(this, "related-changed", {
|
|
||||||
value: { entity: entityId },
|
|
||||||
filter,
|
|
||||||
items,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _devicePicked(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const deviceId = ev.detail.value;
|
|
||||||
if (!deviceId) {
|
|
||||||
fireEvent(this, "related-changed", { value: undefined });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const filter = this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filtered_by_device",
|
|
||||||
{
|
|
||||||
device_name: computeDeviceName(
|
|
||||||
(ev.currentTarget as any).comboBox.selectedItem,
|
|
||||||
this.hass
|
|
||||||
),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const items = await findRelated(this.hass, "device", deviceId);
|
|
||||||
|
|
||||||
fireEvent(this, "related-changed", {
|
|
||||||
value: { device: deviceId },
|
|
||||||
filter,
|
|
||||||
items,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _areaPicked(ev: CustomEvent) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const areaId = ev.detail.value;
|
|
||||||
if (!areaId) {
|
|
||||||
fireEvent(this, "related-changed", { value: undefined });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const filter = this.hass.localize(
|
|
||||||
"ui.components.related-filter-menu.filtered_by_area",
|
|
||||||
{ area_name: (ev.currentTarget as any).comboBox.selectedItem.name }
|
|
||||||
);
|
|
||||||
const items = await findRelated(this.hass, "area", areaId);
|
|
||||||
fireEvent(this, "related-changed", {
|
|
||||||
value: { area: areaId },
|
|
||||||
filter,
|
|
||||||
items,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
:host {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
--mdc-menu-min-width: 250px;
|
|
||||||
}
|
|
||||||
ha-area-picker,
|
|
||||||
ha-device-picker,
|
|
||||||
ha-entity-picker {
|
|
||||||
display: block;
|
|
||||||
width: 300px;
|
|
||||||
padding: 4px 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
ha-area-picker {
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
ha-entity-picker {
|
|
||||||
padding-bottom: 16px;
|
|
||||||
}
|
|
||||||
:host([narrow]) ha-area-picker,
|
|
||||||
:host([narrow]) ha-device-picker,
|
|
||||||
:host([narrow]) ha-entity-picker {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-button-related-filter-menu": HaRelatedFilterButtonMenu;
|
|
||||||
}
|
|
||||||
}
|
|
@ -83,13 +83,11 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues) {
|
protected willUpdate(changedProps: PropertyValues) {
|
||||||
super.willUpdate(changedProps);
|
super.willUpdate(changedProps);
|
||||||
if (changedProps.has("expanded") && this.expanded) {
|
if (changedProps.has("expanded")) {
|
||||||
this._showContent = this.expanded;
|
this._showContent = this.expanded;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Verify we're still expanded
|
// Verify we're still expanded
|
||||||
if (this.expanded) {
|
this._container.style.overflow = this.expanded ? "initial" : "hidden";
|
||||||
this._container.style.overflow = "initial";
|
|
||||||
}
|
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
175
src/components/ha-filter-blueprints.ts
Normal file
175
src/components/ha-filter-blueprints.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import { Blueprints, fetchBlueprints } from "../data/blueprint";
|
||||||
|
|
||||||
|
@customElement("ha-filter-blueprints")
|
||||||
|
export class HaFilterBlueprints extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public type?: "automation" | "script";
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _blueprints?: Blueprints;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.blueprint.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._blueprints && this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._blueprintsSelected}
|
||||||
|
multi
|
||||||
|
class="ha-scrollbar"
|
||||||
|
>
|
||||||
|
${Object.entries(this._blueprints).map(([id, blueprint]) =>
|
||||||
|
"error" in blueprint
|
||||||
|
? nothing
|
||||||
|
: html`<ha-check-list-item
|
||||||
|
.value=${id}
|
||||||
|
.selected=${this.value?.includes(id)}
|
||||||
|
>
|
||||||
|
${blueprint.metadata.name || id}
|
||||||
|
</ha-check-list-item>`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async firstUpdated() {
|
||||||
|
if (!this.type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._blueprints = await fetchBlueprints(this.hass, this.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.narrow || !this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _blueprintsSelected(
|
||||||
|
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||||
|
) {
|
||||||
|
const blueprints = this._blueprints!;
|
||||||
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
|
if (!ev.detail.index.size) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const index of ev.detail.index) {
|
||||||
|
const blueprintId = Object.keys(blueprints)[index];
|
||||||
|
value.push(blueprintId);
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(
|
||||||
|
findRelated(this.hass, `${this.type}_blueprint`, blueprintId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
const results = await Promise.all(relatedPromises);
|
||||||
|
const items: Set<string> = new Set();
|
||||||
|
for (const result of results) {
|
||||||
|
if (result[this.type!]) {
|
||||||
|
result[this.type!]!.forEach((item) => items.add(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: this.type ? items : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-blueprints": HaFilterBlueprints;
|
||||||
|
}
|
||||||
|
}
|
284
src/components/ha-filter-categories.ts
Normal file
284
src/components/ha-filter-categories.ts
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
import { ActionDetail, SelectedDetail } from "@material/mwc-list";
|
||||||
|
import { mdiDelete, mdiDotsVertical, mdiPencil, mdiPlus } from "@mdi/js";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
CategoryRegistryEntry,
|
||||||
|
deleteCategoryRegistryEntry,
|
||||||
|
subscribeCategoryRegistry,
|
||||||
|
} from "../data/category_registry";
|
||||||
|
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { showCategoryRegistryDetailDialog } from "../panels/config/category/show-dialog-category-registry-detail";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-list-item";
|
||||||
|
|
||||||
|
@customElement("ha-filter-categories")
|
||||||
|
export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public scope?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _categories: CategoryRegistryEntry[] = [];
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
protected hassSubscribeRequiredHostProps = ["scope"];
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeCategoryRegistry(
|
||||||
|
this.hass.connection,
|
||||||
|
this.scope!,
|
||||||
|
(categories) => {
|
||||||
|
this._categories = categories;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.category.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._categorySelected}
|
||||||
|
class="ha-scrollbar"
|
||||||
|
activatable
|
||||||
|
>
|
||||||
|
<ha-list-item
|
||||||
|
.selected=${!this.value?.length}
|
||||||
|
.activated=${!this.value?.length}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.category.filter.show_all"
|
||||||
|
)}</ha-list-item
|
||||||
|
>
|
||||||
|
${this._categories.map(
|
||||||
|
(category) =>
|
||||||
|
html`<ha-list-item
|
||||||
|
.value=${category.category_id}
|
||||||
|
.selected=${this.value?.includes(category.category_id)}
|
||||||
|
.activated=${this.value?.includes(category.category_id)}
|
||||||
|
graphic="icon"
|
||||||
|
hasMeta
|
||||||
|
>
|
||||||
|
${category.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="graphic"
|
||||||
|
.icon=${category.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${category.name}
|
||||||
|
<ha-button-menu
|
||||||
|
@action=${this._handleAction}
|
||||||
|
slot="meta"
|
||||||
|
fixed
|
||||||
|
.categoryId=${category.category_id}
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiDotsVertical}
|
||||||
|
slot="trigger"
|
||||||
|
></ha-icon-button>
|
||||||
|
<mwc-list-item graphic="icon"
|
||||||
|
><ha-svg-icon
|
||||||
|
.path=${mdiPencil}
|
||||||
|
slot="graphic"
|
||||||
|
></ha-svg-icon
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.edit"
|
||||||
|
)}</mwc-list-item
|
||||||
|
>
|
||||||
|
<mwc-list-item graphic="icon" class="warning"
|
||||||
|
><ha-svg-icon
|
||||||
|
class="warning"
|
||||||
|
.path=${mdiDelete}
|
||||||
|
slot="graphic"
|
||||||
|
></ha-svg-icon
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.delete"
|
||||||
|
)}</mwc-list-item
|
||||||
|
>
|
||||||
|
</ha-button-menu>
|
||||||
|
</ha-list-item>`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
${this.expanded
|
||||||
|
? html`<ha-list-item graphic="icon" @click=${this._addCategory}>
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||||
|
</ha-list-item>`
|
||||||
|
: nothing}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - (49 + 48)}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
const categoryId = (ev.currentTarget as any).categoryId;
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._editCategory(categoryId);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._deleteCategory(categoryId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _editCategory(id: string) {
|
||||||
|
showCategoryRegistryDetailDialog(this, {
|
||||||
|
scope: this.scope!,
|
||||||
|
entry: this._categories.find((cat) => cat.category_id === id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _deleteCategory(id: string) {
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.confirm_delete"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.confirm_delete_text"
|
||||||
|
),
|
||||||
|
confirmText: this.hass.localize("ui.common.delete"),
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await deleteCategoryRegistryEntry(this.hass, this.scope!, id);
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(`Failed to delete: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addCategory() {
|
||||||
|
if (!this.scope) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showCategoryRegistryDetailDialog(this, { scope: this.scope });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _categorySelected(ev: CustomEvent<SelectedDetail<number>>) {
|
||||||
|
if (!ev.detail.index) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = ev.detail.index - 1;
|
||||||
|
|
||||||
|
const val = this._categories![index]?.category_id;
|
||||||
|
if (!val) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.value = [val];
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: this.value,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
|
mwc-list {
|
||||||
|
--mdc-list-item-meta-size: auto;
|
||||||
|
--mdc-list-side-padding-right: 4px;
|
||||||
|
--mdc-icon-button-size: 36px;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-categories": HaFilterCategories;
|
||||||
|
}
|
||||||
|
}
|
206
src/components/ha-filter-devices.ts
Normal file
206
src/components/ha-filter-devices.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import { computeDeviceName } from "../data/device_registry";
|
||||||
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
|
|
||||||
|
@customElement("ha-filter-devices")
|
||||||
|
export class HaFilterDevices extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public type?: keyof RelatedResult;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
public willUpdate(properties: PropertyValues) {
|
||||||
|
super.willUpdate(properties);
|
||||||
|
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
loadVirtualizer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`<mwc-list class="ha-scrollbar">
|
||||||
|
<lit-virtualizer
|
||||||
|
.items=${this._devices(this.hass.devices)}
|
||||||
|
.renderItem=${this._renderItem}
|
||||||
|
@click=${this._handleItemClick}
|
||||||
|
>
|
||||||
|
</lit-virtualizer>
|
||||||
|
</mwc-list>`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderItem = (device) =>
|
||||||
|
html`<ha-check-list-item
|
||||||
|
.value=${device.id}
|
||||||
|
.selected=${this.value?.includes(device.id)}
|
||||||
|
>
|
||||||
|
${computeDeviceName(device, this.hass)}
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
|
||||||
|
private _handleItemClick(ev) {
|
||||||
|
const listItem = ev.target.closest("ha-check-list-item");
|
||||||
|
const value = listItem?.value;
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.value?.includes(value)) {
|
||||||
|
this.value = this.value?.filter((val) => val !== value);
|
||||||
|
} else {
|
||||||
|
this.value = [...(this.value || []), value];
|
||||||
|
}
|
||||||
|
listItem.selected = this.value?.includes(value);
|
||||||
|
this._findRelated();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _devices = memoizeOne((devices: HomeAssistant["devices"]) => {
|
||||||
|
const values = Object.values(devices);
|
||||||
|
return values.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
|
a.name_by_user || a.name || "",
|
||||||
|
b.name_by_user || b.name || "",
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
private async _findRelated() {
|
||||||
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
|
if (!this.value?.length) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const deviceId of this.value) {
|
||||||
|
value.push(deviceId);
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
const results = await Promise.all(relatedPromises);
|
||||||
|
const items: Set<string> = new Set();
|
||||||
|
for (const result of results) {
|
||||||
|
if (result[this.type!]) {
|
||||||
|
result[this.type!]!.forEach((item) => items.add(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: this.type ? items : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
|
ha-check-list-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-devices": HaFilterDevices;
|
||||||
|
}
|
||||||
|
}
|
220
src/components/ha-filter-entities.ts
Normal file
220
src/components/ha-filter-entities.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-state-icon";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
|
|
||||||
|
@customElement("ha-filter-entities")
|
||||||
|
export class HaFilterEntities extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property() public type?: keyof RelatedResult;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
public willUpdate(properties: PropertyValues) {
|
||||||
|
super.willUpdate(properties);
|
||||||
|
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
loadVirtualizer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list class="ha-scrollbar">
|
||||||
|
<lit-virtualizer
|
||||||
|
.items=${this._entities(this.hass.states, this.type)}
|
||||||
|
.renderItem=${this._renderItem}
|
||||||
|
@click=${this._handleItemClick}
|
||||||
|
>
|
||||||
|
</lit-virtualizer>
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderItem = (entity) =>
|
||||||
|
html`<ha-check-list-item
|
||||||
|
.value=${entity.entity_id}
|
||||||
|
.selected=${this.value?.includes(entity.entity_id)}
|
||||||
|
graphic="icon"
|
||||||
|
>
|
||||||
|
<ha-state-icon
|
||||||
|
slot="graphic"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${entity}
|
||||||
|
></ha-state-icon>
|
||||||
|
${computeStateName(entity)}
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
|
||||||
|
private _handleItemClick(ev) {
|
||||||
|
const listItem = ev.target.closest("ha-check-list-item");
|
||||||
|
const value = listItem?.value;
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.value?.includes(value)) {
|
||||||
|
this.value = this.value?.filter((val) => val !== value);
|
||||||
|
} else {
|
||||||
|
this.value = [...(this.value || []), value];
|
||||||
|
}
|
||||||
|
listItem.selected = this.value?.includes(value);
|
||||||
|
this._findRelated();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _entities = memoizeOne(
|
||||||
|
(states: HomeAssistant["states"], type: this["type"]) => {
|
||||||
|
const values = Object.values(states);
|
||||||
|
return values
|
||||||
|
.filter(
|
||||||
|
(entityState) => !type || computeStateDomain(entityState) !== type
|
||||||
|
)
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
|
computeStateName(a),
|
||||||
|
computeStateName(b),
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _findRelated() {
|
||||||
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
|
if (!this.value?.length) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const entityId of this.value) {
|
||||||
|
value.push(entityId);
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(findRelated(this.hass, "entity", entityId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
const results = await Promise.all(relatedPromises);
|
||||||
|
const items: Set<string> = new Set();
|
||||||
|
for (const result of results) {
|
||||||
|
if (result[this.type!]) {
|
||||||
|
result[this.type!]!.forEach((item) => items.add(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: this.type ? items : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
|
ha-check-list-item {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-entities": HaFilterEntities;
|
||||||
|
}
|
||||||
|
}
|
287
src/components/ha-filter-floor-areas.ts
Normal file
287
src/components/ha-filter-floor-areas.ts
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
FloorRegistryEntry,
|
||||||
|
getFloorAreaLookup,
|
||||||
|
subscribeFloorRegistry,
|
||||||
|
} from "../data/floor_registry";
|
||||||
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
|
||||||
|
@customElement("ha-filter-floor-areas")
|
||||||
|
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: {
|
||||||
|
floors?: string[];
|
||||||
|
areas?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
@property() public type?: keyof RelatedResult;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
@state() private _floors?: FloorRegistryEntry[];
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const areas = this._areas(this.hass.areas, this._floors);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.areas.caption")}
|
||||||
|
${this.value?.areas?.length || this.value?.floors?.length
|
||||||
|
? html`<div class="badge">
|
||||||
|
${(this.value?.areas?.length || 0) +
|
||||||
|
(this.value?.floors?.length || 0)}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list class="ha-scrollbar">
|
||||||
|
${repeat(
|
||||||
|
areas?.floors || [],
|
||||||
|
(floor) => floor.floor_id,
|
||||||
|
(floor) => html`
|
||||||
|
<ha-check-list-item
|
||||||
|
.value=${floor.floor_id}
|
||||||
|
.type=${"floors"}
|
||||||
|
.selected=${this.value?.floors?.includes(
|
||||||
|
floor.floor_id
|
||||||
|
) || false}
|
||||||
|
graphic="icon"
|
||||||
|
@request-selected=${this._handleItemClick}
|
||||||
|
>
|
||||||
|
${floor.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="graphic"
|
||||||
|
.icon=${floor.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${floor.name}
|
||||||
|
</ha-check-list-item>
|
||||||
|
${repeat(
|
||||||
|
floor.areas,
|
||||||
|
(area) => area.area_id,
|
||||||
|
(area) => this._renderArea(area)
|
||||||
|
)}
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
${repeat(
|
||||||
|
areas?.unassisgnedAreas,
|
||||||
|
(area) => area.area_id,
|
||||||
|
(area) => this._renderArea(area)
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderArea(area) {
|
||||||
|
return html`<ha-check-list-item
|
||||||
|
.value=${area.area_id}
|
||||||
|
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||||
|
.type=${"areas"}
|
||||||
|
graphic="icon"
|
||||||
|
class=${area.floor_id ? "floor" : ""}
|
||||||
|
@request-selected=${this._handleItemClick}
|
||||||
|
>
|
||||||
|
${area.icon
|
||||||
|
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${area.name}
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleItemClick(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
const listItem = ev.currentTarget;
|
||||||
|
const type = listItem?.type;
|
||||||
|
const value = listItem?.value;
|
||||||
|
|
||||||
|
if (ev.detail.selected === listItem.selected || !value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value?.[type]?.includes(value)) {
|
||||||
|
this.value = {
|
||||||
|
...this.value,
|
||||||
|
[type]: this.value[type].filter((val) => val !== value),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!this.value) {
|
||||||
|
this.value = {};
|
||||||
|
}
|
||||||
|
this.value = {
|
||||||
|
...this.value,
|
||||||
|
[type]: [...(this.value[type] || []), value],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
listItem.selected = this.value[type]?.includes(value);
|
||||||
|
|
||||||
|
this._findRelated();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||||
|
this._floors = floors;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _areas = memoizeOne(
|
||||||
|
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
|
||||||
|
const areas = Object.values(areaReg);
|
||||||
|
|
||||||
|
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||||
|
|
||||||
|
const unassisgnedAreas = areas.filter(
|
||||||
|
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
floors: floors?.map((floor) => ({
|
||||||
|
...floor,
|
||||||
|
areas: floorAreaLookup[floor.floor_id] || [],
|
||||||
|
})),
|
||||||
|
unassisgnedAreas: unassisgnedAreas,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _findRelated() {
|
||||||
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.value ||
|
||||||
|
(!this.value.areas?.length && !this.value.floors?.length)
|
||||||
|
) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: {},
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value.areas) {
|
||||||
|
for (const areaId of this.value.areas) {
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(findRelated(this.hass, "area", areaId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.value.floors) {
|
||||||
|
for (const floorId of this.value.floors) {
|
||||||
|
if (this.type) {
|
||||||
|
relatedPromises.push(findRelated(this.hass, "floor", floorId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(relatedPromises);
|
||||||
|
const items: Set<string> = new Set();
|
||||||
|
for (const result of results) {
|
||||||
|
if (result[this.type!]) {
|
||||||
|
result[this.type!]!.forEach((item) => items.add(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: this.value,
|
||||||
|
items: this.type ? items : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
|
.floor {
|
||||||
|
padding-left: 32px;
|
||||||
|
padding-inline-start: 32px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-floor-areas": HaFilterFloorAreas;
|
||||||
|
}
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"data-table-filter-changed": { value: any; items: Set<string> | undefined };
|
||||||
|
}
|
||||||
|
}
|
183
src/components/ha-filter-integrations.ts
Normal file
183
src/components/ha-filter-integrations.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import {
|
||||||
|
fetchIntegrationManifests,
|
||||||
|
IntegrationManifest,
|
||||||
|
} from "../data/integration";
|
||||||
|
import "./ha-domain-icon";
|
||||||
|
|
||||||
|
@customElement("ha-filter-integrations")
|
||||||
|
export class HaFilterIntegrations extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _manifests?: IntegrationManifest[];
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.integrations.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._manifests && this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._integrationsSelected}
|
||||||
|
multi
|
||||||
|
class="ha-scrollbar"
|
||||||
|
>
|
||||||
|
${this._integrations(this._manifests).map(
|
||||||
|
(integration) =>
|
||||||
|
html`<ha-check-list-item
|
||||||
|
.value=${integration.domain}
|
||||||
|
.selected=${this.value?.includes(integration.domain)}
|
||||||
|
graphic="icon"
|
||||||
|
>
|
||||||
|
<ha-domain-icon
|
||||||
|
slot="graphic"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.domain=${integration.domain}
|
||||||
|
brandFallback
|
||||||
|
></ha-domain-icon>
|
||||||
|
${integration.name || integration.domain}
|
||||||
|
</ha-check-list-item>`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async firstUpdated() {
|
||||||
|
this._manifests = await fetchIntegrationManifests(this.hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _integrations = memoizeOne((manifest: IntegrationManifest[]) =>
|
||||||
|
manifest
|
||||||
|
.filter(
|
||||||
|
(mnfst) =>
|
||||||
|
!mnfst.integration_type ||
|
||||||
|
!["entity", "system", "hardware"].includes(mnfst.integration_type)
|
||||||
|
)
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
|
a.name || a.domain,
|
||||||
|
b.name || b.domain,
|
||||||
|
this.hass.locale.language
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private async _integrationsSelected(
|
||||||
|
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||||
|
) {
|
||||||
|
const integrations = this._integrations(this._manifests!);
|
||||||
|
|
||||||
|
if (!ev.detail.index.size) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const index of ev.detail.index) {
|
||||||
|
const domain = integrations[index].domain;
|
||||||
|
value.push(domain);
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-integrations": HaFilterIntegrations;
|
||||||
|
}
|
||||||
|
}
|
190
src/components/ha-filter-labels.ts
Normal file
190
src/components/ha-filter-labels.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
LabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
} from "../data/label_registry";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./chips/ha-assist-chip";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
|
import "./ha-icon";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
|
||||||
|
@customElement("ha-filter-labels")
|
||||||
|
export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _labels: LabelRegistryEntry[] = [];
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
this._labels = labels;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.hass.localize("ui.panel.config.labels.caption")}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._labelSelected}
|
||||||
|
class="ha-scrollbar"
|
||||||
|
multi
|
||||||
|
>
|
||||||
|
${this._labels.map((label) => {
|
||||||
|
const color = label.color
|
||||||
|
? computeCssColor(label.color)
|
||||||
|
: undefined;
|
||||||
|
return html`<ha-check-list-item
|
||||||
|
.value=${label.label_id}
|
||||||
|
.selected=${this.value?.includes(label.label_id)}
|
||||||
|
hasMeta
|
||||||
|
>
|
||||||
|
<ha-assist-chip
|
||||||
|
.label=${label.name}
|
||||||
|
active
|
||||||
|
style=${color ? `--color: ${color}` : ""}
|
||||||
|
>
|
||||||
|
${label.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="icon"
|
||||||
|
.icon=${label.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
</ha-assist-chip>
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
})}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||||
|
if (!ev.detail.index.size) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const index of ev.detail.index) {
|
||||||
|
const labelId = this._labels[index].label_id;
|
||||||
|
value.push(labelId);
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
ha-assist-chip {
|
||||||
|
border: 1px solid var(--color);
|
||||||
|
--md-assist-chip-icon-size: 16px;
|
||||||
|
--md-assist-chip-leading-space: 12px;
|
||||||
|
--md-assist-chip-trailing-space: 12px;
|
||||||
|
--ha-assist-chip-active-container-color: var(--color);
|
||||||
|
--ha-assist-chip-active-container-opacity: 0.3;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-labels": HaFilterLabels;
|
||||||
|
}
|
||||||
|
}
|
165
src/components/ha-filter-states.ts
Normal file
165
src/components/ha-filter-states.ts
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import "./ha-expansion-panel";
|
||||||
|
import "./ha-check-list-item";
|
||||||
|
import "./ha-icon";
|
||||||
|
|
||||||
|
@customElement("ha-filter-states")
|
||||||
|
export class HaFilterStates extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public value?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public states?: {
|
||||||
|
value: any;
|
||||||
|
label?: string;
|
||||||
|
icon?: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||||
|
|
||||||
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.states) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
const hasIcon = this.states.find((item) => item.icon);
|
||||||
|
return html`
|
||||||
|
<ha-expansion-panel
|
||||||
|
leftChevron
|
||||||
|
.expanded=${this.expanded}
|
||||||
|
@expanded-will-change=${this._expandedWillChange}
|
||||||
|
@expanded-changed=${this._expandedChanged}
|
||||||
|
>
|
||||||
|
<div slot="header" class="header">
|
||||||
|
${this.label}
|
||||||
|
${this.value?.length
|
||||||
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
${this._shouldRender
|
||||||
|
? html`
|
||||||
|
<mwc-list
|
||||||
|
@selected=${this._statesSelected}
|
||||||
|
multi
|
||||||
|
class="ha-scrollbar"
|
||||||
|
>
|
||||||
|
${this.states.map(
|
||||||
|
(item) =>
|
||||||
|
html`<ha-check-list-item
|
||||||
|
.value=${item.value}
|
||||||
|
.selected=${this.value?.includes(item.value)}
|
||||||
|
.graphic=${hasIcon ? "icon" : undefined}
|
||||||
|
>
|
||||||
|
${item.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="graphic"
|
||||||
|
.icon=${item.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${item.label}
|
||||||
|
</ha-check-list-item>`
|
||||||
|
)}
|
||||||
|
</mwc-list>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-expansion-panel>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changed) {
|
||||||
|
if (changed.has("expanded") && this.expanded) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.expanded) return;
|
||||||
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
|
`${this.clientHeight - 49}px`;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedWillChange(ev) {
|
||||||
|
this._shouldRender = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _expandedChanged(ev) {
|
||||||
|
this.expanded = ev.detail.expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _statesSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||||
|
if (!ev.detail.index.size) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
|
for (const index of ev.detail.index) {
|
||||||
|
const val = this.states![index].value;
|
||||||
|
value.push(val);
|
||||||
|
}
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value,
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleScrollbar,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
:host([expanded]) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
ha-expansion-panel {
|
||||||
|
--ha-card-border-radius: 0;
|
||||||
|
--expansion-panel-content-padding: 0;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
margin-inline-end: 0;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-filter-states": HaFilterStates;
|
||||||
|
}
|
||||||
|
}
|
112
src/components/search-input-outlined.ts
Normal file
112
src/components/search-input-outlined.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import "@material/web/textfield/outlined-text-field";
|
||||||
|
import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field";
|
||||||
|
import { mdiMagnify } from "@mdi/js";
|
||||||
|
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||||
|
import { customElement, property, query } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-icon-button";
|
||||||
|
import "./ha-svg-icon";
|
||||||
|
|
||||||
|
@customElement("search-input-outlined")
|
||||||
|
class SearchInputOutlined extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public filter?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public suffix = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public autofocus = false;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public label?: string;
|
||||||
|
|
||||||
|
@property({ type: String })
|
||||||
|
public placeholder?: string;
|
||||||
|
|
||||||
|
public focus() {
|
||||||
|
this._input?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@query("md-outlined-text-field", true) private _input!: MdOutlinedTextField;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<md-outlined-text-field
|
||||||
|
.autofocus=${this.autofocus}
|
||||||
|
.aria-label=${this.label || this.hass.localize("ui.common.search")}
|
||||||
|
.placeholder=${this.placeholder ||
|
||||||
|
this.hass.localize("ui.common.search")}
|
||||||
|
.value=${this.filter || ""}
|
||||||
|
icon
|
||||||
|
.iconTrailing=${this.filter || this.suffix}
|
||||||
|
@input=${this._filterInputChanged}
|
||||||
|
>
|
||||||
|
<slot name="prefix" slot="leading-icon">
|
||||||
|
<ha-svg-icon
|
||||||
|
tabindex="-1"
|
||||||
|
class="prefix"
|
||||||
|
.path=${mdiMagnify}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</slot>
|
||||||
|
</md-outlined-text-field>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _filterChanged(value: string) {
|
||||||
|
fireEvent(this, "value-changed", { value: String(value) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _filterInputChanged(e) {
|
||||||
|
this._filterChanged(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
md-outlined-text-field {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
--md-sys-color-on-surface: var(--primary-text-color);
|
||||||
|
--md-sys-color-primary: var(--primary-text-color);
|
||||||
|
--md-outlined-text-field-input-text-color: var(--primary-text-color);
|
||||||
|
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||||
|
--md-outlined-field-top-space: 5.5px;
|
||||||
|
--md-outlined-field-bottom-space: 5.5px;
|
||||||
|
--md-outlined-field-outline-color: var(--outline-color);
|
||||||
|
--md-outlined-field-container-shape-start-start: 10px;
|
||||||
|
--md-outlined-field-container-shape-start-end: 10px;
|
||||||
|
--md-outlined-field-container-shape-end-end: 10px;
|
||||||
|
--md-outlined-field-container-shape-end-start: 10px;
|
||||||
|
--md-outlined-field-focus-outline-width: 1px;
|
||||||
|
--md-outlined-field-focus-outline-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
ha-svg-icon,
|
||||||
|
ha-icon-button {
|
||||||
|
display: flex;
|
||||||
|
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
ha-svg-icon {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.clear-button {
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
}
|
||||||
|
.trailing {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"search-input-outlined": SearchInputOutlined;
|
||||||
|
}
|
||||||
|
}
|
86
src/data/category_registry.ts
Normal file
86
src/data/category_registry.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||||
|
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||||
|
import { stringCompare } from "../common/string/compare";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import { debounce } from "../common/util/debounce";
|
||||||
|
|
||||||
|
export interface CategoryRegistryEntry {
|
||||||
|
category_id: string;
|
||||||
|
name: string;
|
||||||
|
icon: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategoryRegistryEntryMutableParams {
|
||||||
|
name: string;
|
||||||
|
icon?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchCategoryRegistry = (conn: Connection, scope: string) =>
|
||||||
|
conn
|
||||||
|
.sendMessagePromise<CategoryRegistryEntry[]>({
|
||||||
|
type: "config/category_registry/list",
|
||||||
|
scope,
|
||||||
|
})
|
||||||
|
.then((categories) =>
|
||||||
|
categories.sort((ent1, ent2) => stringCompare(ent1.name, ent2.name))
|
||||||
|
);
|
||||||
|
|
||||||
|
export const subscribeCategoryRegistry = (
|
||||||
|
conn: Connection,
|
||||||
|
scope: string,
|
||||||
|
onChange: (floors: CategoryRegistryEntry[]) => void
|
||||||
|
) =>
|
||||||
|
createCollection<CategoryRegistryEntry[]>(
|
||||||
|
`_categoryRegistry_${scope}`,
|
||||||
|
(conn2: Connection) => fetchCategoryRegistry(conn2, scope),
|
||||||
|
(conn2: Connection, store: Store<CategoryRegistryEntry[]>) =>
|
||||||
|
conn2.subscribeEvents(
|
||||||
|
debounce(
|
||||||
|
() =>
|
||||||
|
fetchCategoryRegistry(conn2, scope).then(
|
||||||
|
(categories: CategoryRegistryEntry[]) =>
|
||||||
|
store.setState(categories, true)
|
||||||
|
),
|
||||||
|
500,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
"category_registry_updated"
|
||||||
|
),
|
||||||
|
conn,
|
||||||
|
onChange
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createCategoryRegistryEntry = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
scope: string,
|
||||||
|
values: CategoryRegistryEntryMutableParams
|
||||||
|
) =>
|
||||||
|
hass.callWS<CategoryRegistryEntry>({
|
||||||
|
type: "config/category_registry/create",
|
||||||
|
scope,
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateCategoryRegistryEntry = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
scope: string,
|
||||||
|
category_id: string,
|
||||||
|
updates: Partial<CategoryRegistryEntryMutableParams>
|
||||||
|
) =>
|
||||||
|
hass.callWS<CategoryRegistryEntry>({
|
||||||
|
type: "config/category_registry/update",
|
||||||
|
scope,
|
||||||
|
category_id,
|
||||||
|
...updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteCategoryRegistryEntry = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
scope: string,
|
||||||
|
category_id: string
|
||||||
|
) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "config/category_registry/delete",
|
||||||
|
scope,
|
||||||
|
category_id,
|
||||||
|
});
|
@ -61,6 +61,7 @@ export interface EntityRegistryEntry {
|
|||||||
unique_id: string;
|
unique_id: string;
|
||||||
translation_key?: string;
|
translation_key?: string;
|
||||||
options: EntityRegistryOptions | null;
|
options: EntityRegistryOptions | null;
|
||||||
|
categories: { [scope: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||||
@ -137,6 +138,7 @@ export interface EntityRegistryEntryUpdateParams {
|
|||||||
| LightEntityOptions;
|
| LightEntityOptions;
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
|
categories?: { [scope: string]: string | null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const batteryPriorities = ["sensor", "binary_sensor"];
|
const batteryPriorities = ["sensor", "binary_sensor"];
|
||||||
|
@ -26,6 +26,7 @@ export type ItemType =
|
|||||||
| "config_entry"
|
| "config_entry"
|
||||||
| "device"
|
| "device"
|
||||||
| "entity"
|
| "entity"
|
||||||
|
| "floor"
|
||||||
| "group"
|
| "group"
|
||||||
| "scene"
|
| "scene"
|
||||||
| "script"
|
| "script"
|
||||||
|
@ -1,15 +1,37 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import "@material/mwc-button/mwc-button";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import {
|
||||||
|
mdiArrowDown,
|
||||||
|
mdiArrowUp,
|
||||||
|
mdiClose,
|
||||||
|
mdiFilterRemove,
|
||||||
|
mdiFilterVariant,
|
||||||
|
mdiFormatListChecks,
|
||||||
|
mdiMenuDown,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import {
|
||||||
|
CSSResultGroup,
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
|
} from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { LocalizeFunc } from "../common/translations/localize";
|
import { LocalizeFunc } from "../common/translations/localize";
|
||||||
|
import "../components/chips/ha-assist-chip";
|
||||||
|
import "../components/chips/ha-filter-chip";
|
||||||
import "../components/data-table/ha-data-table";
|
import "../components/data-table/ha-data-table";
|
||||||
import type {
|
import type {
|
||||||
DataTableColumnContainer,
|
DataTableColumnContainer,
|
||||||
DataTableRowData,
|
DataTableRowData,
|
||||||
HaDataTable,
|
HaDataTable,
|
||||||
|
SortingDirection,
|
||||||
} from "../components/data-table/ha-data-table";
|
} from "../components/data-table/ha-data-table";
|
||||||
|
import "../components/ha-dialog";
|
||||||
|
import "../components/search-input-outlined";
|
||||||
import type { HomeAssistant, Route } from "../types";
|
import type { HomeAssistant, Route } from "../types";
|
||||||
import "./hass-tabs-subpage";
|
import "./hass-tabs-subpage";
|
||||||
import type { PageNavigation } from "./hass-tabs-subpage";
|
import type { PageNavigation } from "./hass-tabs-subpage";
|
||||||
@ -87,22 +109,16 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
@property() public searchLabel?: string;
|
@property() public searchLabel?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of strings that show what the data is currently filtered by.
|
* Number of active filters.
|
||||||
* @type {Array}
|
|
||||||
*/
|
|
||||||
@property({ type: Array }) public activeFilters?;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Text to how how many items are hidden.
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
@property() public hiddenLabel?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* How many items are hidden because of active filters.
|
|
||||||
* @type {Number}
|
* @type {Number}
|
||||||
*/
|
*/
|
||||||
@property({ type: Number }) public numHidden = 0;
|
@property({ type: Number }) public filters?;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of current selections.
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
@property({ type: Number }) public selected?;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What path to use when the back button is pressed.
|
* What path to use when the back button is pressed.
|
||||||
@ -138,57 +154,146 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
@property({ attribute: false }) public tabs: PageNavigation[] = [];
|
@property({ attribute: false }) public tabs: PageNavigation[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force hides the filter menu.
|
* Show the filter menu.
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
*/
|
*/
|
||||||
@property({ type: Boolean }) public hideFilterMenu = false;
|
@property({ type: Boolean }) public hasFilters = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public showFilters = false;
|
||||||
|
|
||||||
|
@property() public initialGroupColumn?: string;
|
||||||
|
|
||||||
|
@state() private _sortColumn?: string;
|
||||||
|
|
||||||
|
@state() private _sortDirection: SortingDirection = null;
|
||||||
|
|
||||||
|
@state() private _groupColumn?: string;
|
||||||
|
|
||||||
|
@state() private _selectMode = false;
|
||||||
|
|
||||||
@query("ha-data-table", true) private _dataTable!: HaDataTable;
|
@query("ha-data-table", true) private _dataTable!: HaDataTable;
|
||||||
|
|
||||||
|
private _showPaneController = new ResizeController(this, {
|
||||||
|
callback: (entries) => entries[0]?.contentRect.width > 750,
|
||||||
|
});
|
||||||
|
|
||||||
public clearSelection() {
|
public clearSelection() {
|
||||||
this._dataTable.clearSelection();
|
this._dataTable.clearSelection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected firstUpdated() {
|
||||||
|
if (this.initialGroupColumn) {
|
||||||
|
this._groupColumn = this.initialGroupColumn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
const hiddenLabel = this.numHidden
|
const localize = this.localizeFunc || this.hass.localize;
|
||||||
? this.hiddenLabel ||
|
const showPane = this._showPaneController.value ?? !this.narrow;
|
||||||
this.hass.localize("ui.components.data-table.hidden", {
|
const filterButton = this.hasFilters
|
||||||
number: this.numHidden,
|
? html`<div class="relative">
|
||||||
}) ||
|
<ha-assist-chip
|
||||||
this.numHidden
|
.label=${localize("ui.components.subpage-data-table.filters")}
|
||||||
: undefined;
|
.active=${this.filters}
|
||||||
|
@click=${this._toggleFilters}
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiFilterVariant}></ha-svg-icon>
|
||||||
|
</ha-assist-chip>
|
||||||
|
${this.filters
|
||||||
|
? html`<div class="badge">${this.filters}</div>`
|
||||||
|
: nothing}
|
||||||
|
</div>`
|
||||||
|
: nothing;
|
||||||
|
|
||||||
const filterInfo = this.activeFilters
|
const selectModeBtn =
|
||||||
? html`${this.hass.localize("ui.components.data-table.filtering_by")}
|
this.selectable && !this._selectMode
|
||||||
${this.activeFilters.join(", ")}
|
? html`<ha-assist-chip
|
||||||
${hiddenLabel ? `(${hiddenLabel})` : ""}`
|
class="has-dropdown select-mode-chip"
|
||||||
: hiddenLabel;
|
.active=${this._selectMode}
|
||||||
|
@click=${this._enableSelectMode}
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiFormatListChecks}></ha-svg-icon>
|
||||||
|
</ha-assist-chip>`
|
||||||
|
: nothing;
|
||||||
|
|
||||||
const headerToolbar = html`<search-input
|
const searchBar = html`<search-input-outlined
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.filter=${this.filter}
|
.filter=${this.filter}
|
||||||
.suffix=${!this.narrow}
|
|
||||||
@value-changed=${this._handleSearchChange}
|
@value-changed=${this._handleSearchChange}
|
||||||
.label=${this.searchLabel}
|
.label=${this.searchLabel}
|
||||||
|
.placeholder=${this.searchLabel}
|
||||||
>
|
>
|
||||||
${!this.narrow
|
</search-input-outlined>`;
|
||||||
? html`<div
|
|
||||||
class="filters"
|
const sortByMenu = Object.values(this.columns).find((col) => col.sortable)
|
||||||
slot="suffix"
|
? html`<ha-button-menu fixed>
|
||||||
@click=${this._preventDefault}
|
<ha-assist-chip
|
||||||
|
.label=${localize("ui.components.subpage-data-table.sort_by", {
|
||||||
|
sortColumn: this._sortColumn
|
||||||
|
? ` ${this.columns[this._sortColumn].title || this.columns[this._sortColumn].label}`
|
||||||
|
: "",
|
||||||
|
})}
|
||||||
|
slot="trigger"
|
||||||
>
|
>
|
||||||
${filterInfo
|
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon
|
||||||
? html`<div class="active-filters">
|
></ha-assist-chip>
|
||||||
${filterInfo}
|
${Object.entries(this.columns).map(([id, column]) =>
|
||||||
<mwc-button @click=${this._clearFilter}>
|
column.sortable
|
||||||
${this.hass.localize("ui.components.data-table.clear")}
|
? html`<ha-list-item
|
||||||
</mwc-button>
|
.value=${id}
|
||||||
</div>`
|
@request-selected=${this._handleSortBy}
|
||||||
: ""}
|
hasMeta
|
||||||
<slot name="filter-menu"></slot>
|
.activated=${id === this._sortColumn}
|
||||||
</div>`
|
>
|
||||||
: ""}
|
${this._sortColumn === id
|
||||||
</search-input>`;
|
? html`<ha-svg-icon
|
||||||
|
slot="meta"
|
||||||
|
.path=${this._sortDirection === "desc"
|
||||||
|
? mdiArrowDown
|
||||||
|
: mdiArrowUp}
|
||||||
|
></ha-svg-icon>`
|
||||||
|
: nothing}
|
||||||
|
${column.title || column.label}
|
||||||
|
</ha-list-item>`
|
||||||
|
: nothing
|
||||||
|
)}
|
||||||
|
</ha-button-menu>`
|
||||||
|
: nothing;
|
||||||
|
|
||||||
|
const groupByMenu = Object.values(this.columns).find((col) => col.groupable)
|
||||||
|
? html`<ha-button-menu fixed>
|
||||||
|
<ha-assist-chip
|
||||||
|
.label=${localize("ui.components.subpage-data-table.group_by", {
|
||||||
|
groupColumn: this._groupColumn
|
||||||
|
? ` ${this.columns[this._groupColumn].title || this.columns[this._groupColumn].label}`
|
||||||
|
: "",
|
||||||
|
})}
|
||||||
|
slot="trigger"
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon
|
||||||
|
></ha-assist-chip>
|
||||||
|
${Object.entries(this.columns).map(([id, column]) =>
|
||||||
|
column.groupable
|
||||||
|
? html`<ha-list-item
|
||||||
|
.value=${id}
|
||||||
|
@request-selected=${this._handleGroupBy}
|
||||||
|
.activated=${id === this._groupColumn}
|
||||||
|
>
|
||||||
|
${column.title || column.label}
|
||||||
|
</ha-list-item> `
|
||||||
|
: nothing
|
||||||
|
)}
|
||||||
|
<li divider role="separator"></li>
|
||||||
|
<ha-list-item
|
||||||
|
.value=${undefined}
|
||||||
|
@request-selected=${this._handleGroupBy}
|
||||||
|
.activated=${this._groupColumn === undefined}
|
||||||
|
>${localize(
|
||||||
|
"ui.components.subpage-data-table.dont_group_by"
|
||||||
|
)}</ha-list-item
|
||||||
|
>
|
||||||
|
</ha-button-menu>`
|
||||||
|
: nothing;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
@ -202,34 +307,89 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
.tabs=${this.tabs}
|
.tabs=${this.tabs}
|
||||||
.mainPage=${this.mainPage}
|
.mainPage=${this.mainPage}
|
||||||
.supervisor=${this.supervisor}
|
.supervisor=${this.supervisor}
|
||||||
|
.pane=${showPane && this.showFilters}
|
||||||
|
@sorting-changed=${this._sortingChanged}
|
||||||
>
|
>
|
||||||
|
${this._selectMode
|
||||||
|
? html`<div class="selection-bar" slot="toolbar">
|
||||||
|
<div class="center-vertical">
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiClose}
|
||||||
|
@click=${this._disableSelectMode}
|
||||||
|
></ha-icon-button>
|
||||||
|
<p>
|
||||||
|
${localize("ui.components.subpage-data-table.selected", {
|
||||||
|
selected: this.selected || "0",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="center-vertical">
|
||||||
|
<slot name="selection-bar"></slot>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
${this.showFilters
|
||||||
|
? !showPane
|
||||||
|
? html`<ha-dialog
|
||||||
|
open
|
||||||
|
hideActions
|
||||||
|
.heading=${localize("ui.components.subpage-data-table.filters")}
|
||||||
|
>
|
||||||
|
<ha-dialog-header slot="heading">
|
||||||
|
<ha-icon-button
|
||||||
|
slot="navigationIcon"
|
||||||
|
.path=${mdiClose}
|
||||||
|
@click=${this._toggleFilters}
|
||||||
|
></ha-icon-button>
|
||||||
|
<span slot="title"
|
||||||
|
>${localize(
|
||||||
|
"ui.components.subpage-data-table.filters"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
slot="actionItems"
|
||||||
|
.path=${mdiFilterRemove}
|
||||||
|
></ha-icon-button>
|
||||||
|
</ha-dialog-header>
|
||||||
|
<div class="filter-dialog-content">
|
||||||
|
<slot name="filter-pane"></slot></div
|
||||||
|
></ha-dialog>`
|
||||||
|
: html`<div class="pane" slot="pane">
|
||||||
|
<div class="table-header">
|
||||||
|
<ha-assist-chip
|
||||||
|
.label=${localize(
|
||||||
|
"ui.components.subpage-data-table.filters"
|
||||||
|
)}
|
||||||
|
active
|
||||||
|
@click=${this._toggleFilters}
|
||||||
|
>
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="icon"
|
||||||
|
.path=${mdiFilterVariant}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-assist-chip>
|
||||||
|
<ha-icon-button
|
||||||
|
.path=${mdiFilterRemove}
|
||||||
|
@click=${this._clearFilters}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
<div class="pane-content">
|
||||||
|
<slot name="filter-pane"></slot>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
${this.empty
|
${this.empty
|
||||||
? html`<div class="center">
|
? html`<div class="center">
|
||||||
<slot name="empty">${this.noDataText}</slot>
|
<slot name="empty">${this.noDataText}</slot>
|
||||||
</div>`
|
</div>`
|
||||||
: html`${!this.hideFilterMenu
|
: html`<div slot="toolbar-icon">
|
||||||
? html`
|
<slot name="toolbar-icon"></slot>
|
||||||
<div slot="toolbar-icon">
|
</div>
|
||||||
${this.narrow
|
|
||||||
? html`
|
|
||||||
<div class="filter-menu">
|
|
||||||
${this.numHidden || this.activeFilters
|
|
||||||
? html`<span class="badge"
|
|
||||||
>${this.numHidden || "!"}</span
|
|
||||||
>`
|
|
||||||
: ""}
|
|
||||||
<slot name="filter-menu"></slot>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}<slot name="toolbar-icon"></slot>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this.narrow
|
${this.narrow
|
||||||
? html`
|
? html`
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<div class="search-toolbar">${headerToolbar}</div>
|
<div class="search-toolbar">${searchBar}</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
@ -240,30 +400,76 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
.data=${this.data}
|
.data=${this.data}
|
||||||
.noDataText=${this.noDataText}
|
.noDataText=${this.noDataText}
|
||||||
.filter=${this.filter}
|
.filter=${this.filter}
|
||||||
.selectable=${this.selectable}
|
.selectable=${this._selectMode}
|
||||||
.hasFab=${this.hasFab}
|
.hasFab=${this.hasFab}
|
||||||
.id=${this.id}
|
.id=${this.id}
|
||||||
.clickable=${this.clickable}
|
.clickable=${this.clickable}
|
||||||
.appendRow=${this.appendRow}
|
.appendRow=${this.appendRow}
|
||||||
|
.sortColumn=${this._sortColumn}
|
||||||
|
.sortDirection=${this._sortDirection}
|
||||||
|
.groupColumn=${this._groupColumn}
|
||||||
>
|
>
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html`
|
? html`
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<div class="table-header">${headerToolbar}</div>
|
<div class="table-header">
|
||||||
|
${this.hasFilters && !this.showFilters
|
||||||
|
? html`${filterButton}`
|
||||||
|
: nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}
|
||||||
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: html` <div slot="header"></div> `}
|
: html`<div slot="header"></div>
|
||||||
|
<div slot="header-row" class="narrow-header-row">
|
||||||
|
${this.hasFilters && !this.showFilters
|
||||||
|
? html`${filterButton}`
|
||||||
|
: nothing}
|
||||||
|
${selectModeBtn}${groupByMenu}${sortByMenu}
|
||||||
|
</div>`}
|
||||||
</ha-data-table>`}
|
</ha-data-table>`}
|
||||||
|
|
||||||
<div slot="fab"><slot name="fab"></slot></div>
|
<div slot="fab"><slot name="fab"></slot></div>
|
||||||
</hass-tabs-subpage>
|
</hass-tabs-subpage>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _preventDefault(ev) {
|
private _clearFilters() {
|
||||||
ev.preventDefault();
|
fireEvent(this, "clear-filter");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleFilters() {
|
||||||
|
this.showFilters = !this.showFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _sortingChanged(ev) {
|
||||||
|
this._sortDirection = ev.detail.direction;
|
||||||
|
this._sortColumn = this._sortDirection ? ev.detail.column : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSortBy(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const columnId = ev.currentTarget.value;
|
||||||
|
if (!this._sortDirection || this._sortColumn !== columnId) {
|
||||||
|
this._sortDirection = "asc";
|
||||||
|
} else if (this._sortDirection === "asc") {
|
||||||
|
this._sortDirection = "desc";
|
||||||
|
} else {
|
||||||
|
this._sortDirection = null;
|
||||||
|
}
|
||||||
|
this._sortColumn = this._sortDirection === null ? undefined : columnId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleGroupBy(ev) {
|
||||||
|
this._groupColumn = ev.currentTarget.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _enableSelectMode() {
|
||||||
|
this._selectMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _disableSelectMode() {
|
||||||
|
this._selectMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleSearchChange(ev: CustomEvent) {
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
@ -274,54 +480,56 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
fireEvent(this, "search-changed", { value: this.filter });
|
fireEvent(this, "search-changed", { value: this.filter });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearFilter() {
|
|
||||||
fireEvent(this, "clear-filter");
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
ha-data-table {
|
ha-data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
--data-table-border-width: 0;
|
--data-table-border-width: 0;
|
||||||
}
|
}
|
||||||
:host(:not([narrow])) ha-data-table {
|
:host(:not([narrow])) ha-data-table,
|
||||||
|
.pane {
|
||||||
height: calc(100vh - 1px - var(--header-height));
|
height: calc(100vh - 1px - var(--header-height));
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pane-content {
|
||||||
|
height: calc(100vh - 1px - var(--header-height) - var(--header-height));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
:host([narrow]) hass-tabs-subpage {
|
:host([narrow]) hass-tabs-subpage {
|
||||||
--main-title-margin: 0;
|
--main-title-margin: 0;
|
||||||
}
|
}
|
||||||
|
:host([narrow]) {
|
||||||
|
--expansion-panel-summary-padding: 0 16px;
|
||||||
|
}
|
||||||
.table-header {
|
.table-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
--mdc-shape-small: 0;
|
--mdc-shape-small: 0;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
gap: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: var(--primary-background-color);
|
||||||
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
search-input-outlined {
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.search-toolbar {
|
.search-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
search-input {
|
|
||||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
|
||||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
|
||||||
--text-field-overflow: visible;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
.table-header search-input {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
.search-toolbar search-input {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
--mdc-ripple-color: transparant;
|
|
||||||
}
|
|
||||||
.filters {
|
.filters {
|
||||||
--mdc-text-field-fill-color: var(--input-fill-color);
|
--mdc-text-field-fill-color: var(--input-fill-color);
|
||||||
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
|
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
|
||||||
@ -382,9 +590,6 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
top: 4px;
|
top: 4px;
|
||||||
font-size: 0.65em;
|
font-size: 0.65em;
|
||||||
}
|
}
|
||||||
.filter-menu {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.center {
|
.center {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -395,6 +600,92 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -4px;
|
||||||
|
min-width: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 11px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px 2px;
|
||||||
|
color: var(--text-accent-color, var(--text-primary-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow-header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 16px;
|
||||||
|
overflow-x: scroll;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-bar {
|
||||||
|
background: rgba(var(--rgb-primary-color), 0.1);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-vertical {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-bar p {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-assist-chip {
|
||||||
|
--ha-assist-chip-container-shape: 10px;
|
||||||
|
}
|
||||||
|
ha-button-menu {
|
||||||
|
--mdc-list-item-meta-size: 16px;
|
||||||
|
--mdc-list-item-meta-display: flex;
|
||||||
|
}
|
||||||
|
ha-button-menu ha-assist-chip {
|
||||||
|
--md-assist-chip-trailing-space: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-mode-chip {
|
||||||
|
--md-assist-chip-icon-label-space: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-dialog {
|
||||||
|
--mdc-dialog-min-width: calc(
|
||||||
|
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||||
|
);
|
||||||
|
--mdc-dialog-max-width: calc(
|
||||||
|
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||||
|
);
|
||||||
|
--mdc-dialog-min-height: 100%;
|
||||||
|
--mdc-dialog-max-height: 100%;
|
||||||
|
--vertical-align-dialog: flex-end;
|
||||||
|
--ha-dialog-border-radius: 0;
|
||||||
|
--dialog-content-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-dialog-content {
|
||||||
|
height: calc(100vh - 1px - var(--header-height));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
|
nothing,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
@ -57,6 +58,8 @@ class HassTabsSubpage extends LitElement {
|
|||||||
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
|
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
|
||||||
public isWide = false;
|
public isWide = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public pane = false;
|
||||||
|
|
||||||
@state() private _activeTab?: PageNavigation;
|
@state() private _activeTab?: PageNavigation;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -128,49 +131,62 @@ class HassTabsSubpage extends LitElement {
|
|||||||
const showTabs = tabs.length > 1;
|
const showTabs = tabs.length > 1;
|
||||||
return html`
|
return html`
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
${this.mainPage || (!this.backPath && history.state?.root)
|
<slot name="toolbar">
|
||||||
? html`
|
<div class="toolbar-content">
|
||||||
<ha-menu-button
|
${this.mainPage || (!this.backPath && history.state?.root)
|
||||||
.hassio=${this.supervisor}
|
? html`
|
||||||
.hass=${this.hass}
|
<ha-menu-button
|
||||||
.narrow=${this.narrow}
|
.hassio=${this.supervisor}
|
||||||
></ha-menu-button>
|
|
||||||
`
|
|
||||||
: this.backPath
|
|
||||||
? html`
|
|
||||||
<a href=${this.backPath}>
|
|
||||||
<ha-icon-button-arrow-prev
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></ha-icon-button-arrow-prev>
|
.narrow=${this.narrow}
|
||||||
</a>
|
></ha-menu-button>
|
||||||
`
|
`
|
||||||
: html`
|
: this.backPath
|
||||||
<ha-icon-button-arrow-prev
|
? html`
|
||||||
.hass=${this.hass}
|
<a href=${this.backPath}>
|
||||||
@click=${this._backTapped}
|
<ha-icon-button-arrow-prev
|
||||||
></ha-icon-button-arrow-prev>
|
.hass=${this.hass}
|
||||||
`}
|
></ha-icon-button-arrow-prev>
|
||||||
${this.narrow || !showTabs
|
</a>
|
||||||
? html`<div class="main-title">
|
`
|
||||||
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
|
: html`
|
||||||
</div>`
|
<ha-icon-button-arrow-prev
|
||||||
|
.hass=${this.hass}
|
||||||
|
@click=${this._backTapped}
|
||||||
|
></ha-icon-button-arrow-prev>
|
||||||
|
`}
|
||||||
|
${this.narrow || !showTabs
|
||||||
|
? html`<div class="main-title">
|
||||||
|
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
${showTabs && !this.narrow
|
||||||
|
? html`<div id="tabbar">${tabs}</div>`
|
||||||
|
: ""}
|
||||||
|
<div id="toolbar-icon">
|
||||||
|
<slot name="toolbar-icon"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
${showTabs && this.narrow
|
||||||
|
? html`<div id="tabbar" class="bottom-bar">${tabs}</div>`
|
||||||
: ""}
|
: ""}
|
||||||
${showTabs
|
|
||||||
? html`
|
|
||||||
<div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}>
|
|
||||||
${tabs}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
<div id="toolbar-icon">
|
|
||||||
<slot name="toolbar-icon"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="container">
|
||||||
class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
|
${this.pane
|
||||||
@scroll=${this._saveScrollPos}
|
? html`<div class="pane">
|
||||||
>
|
<div class="shadow-container"></div>
|
||||||
<slot></slot>
|
<div class="ha-scrollbar">
|
||||||
|
<slot name="pane"></slot>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
<div
|
||||||
|
class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
|
||||||
|
@scroll=${this._saveScrollPos}
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="fab" class=${classMap({ tabs: showTabs })}>
|
<div id="fab" class=${classMap({ tabs: showTabs })}>
|
||||||
<slot name="fab"></slot>
|
<slot name="fab"></slot>
|
||||||
@ -206,6 +222,15 @@ class HassTabsSubpage extends LitElement {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100% - var(--header-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
:host([narrow]) .container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
ha-menu-button {
|
ha-menu-button {
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
margin-inline-end: 24px;
|
margin-inline-end: 24px;
|
||||||
@ -213,18 +238,22 @@ class HassTabsSubpage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
height: var(--header-height);
|
height: var(--header-height);
|
||||||
background-color: var(--sidebar-background-color);
|
background-color: var(--sidebar-background-color);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
border-bottom: 1px solid var(--divider-color);
|
border-bottom: 1px solid var(--divider-color);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.toolbar-content {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@media (max-width: 599px) {
|
@media (max-width: 599px) {
|
||||||
.toolbar {
|
.toolbar-content {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -297,10 +326,6 @@ class HassTabsSubpage extends LitElement {
|
|||||||
margin-right: env(safe-area-inset-right);
|
margin-right: env(safe-area-inset-right);
|
||||||
margin-inline-start: env(safe-area-inset-left);
|
margin-inline-start: env(safe-area-inset-left);
|
||||||
margin-inline-end: env(safe-area-inset-right);
|
margin-inline-end: env(safe-area-inset-right);
|
||||||
height: calc(100% - 1px - var(--header-height));
|
|
||||||
height: calc(
|
|
||||||
100% - 1px - var(--header-height) - env(safe-area-inset-bottom)
|
|
||||||
);
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
@ -329,6 +354,21 @@ class HassTabsSubpage extends LitElement {
|
|||||||
inset-inline-end: 24px;
|
inset-inline-end: 24px;
|
||||||
inset-inline-start: initial;
|
inset-inline-start: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pane {
|
||||||
|
border-right: 1px solid var(--divider-color);
|
||||||
|
border-inline-end: 1px solid var(--divider-color);
|
||||||
|
border-inline-start: initial;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 var(--sidepane-width, 250px);
|
||||||
|
width: var(--sidepane-width, 250px);
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.pane .ha-scrollbar {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import "@material/mwc-list/mwc-list";
|
|
||||||
import "@material/web/divider/divider";
|
import "@material/web/divider/divider";
|
||||||
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
|
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
|
||||||
import Fuse, { IFuseOptions } from "fuse.js";
|
import Fuse, { IFuseOptions } from "fuse.js";
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { consume } from "@lit-labs/context";
|
||||||
|
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||||
import {
|
import {
|
||||||
mdiCancel,
|
|
||||||
mdiContentDuplicate,
|
mdiContentDuplicate,
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
mdiHelpCircle,
|
mdiHelpCircle,
|
||||||
@ -9,34 +10,41 @@ import {
|
|||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiRobotHappy,
|
mdiRobotHappy,
|
||||||
mdiStopCircleOutline,
|
mdiStopCircleOutline,
|
||||||
|
mdiTag,
|
||||||
mdiTransitConnection,
|
mdiTransitConnection,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
import { differenceInDays } from "date-fns/esm";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
nothing,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { differenceInDays } from "date-fns/esm";
|
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||||
import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
|
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||||
import { navigate } from "../../../common/navigate";
|
import { navigate } from "../../../common/navigate";
|
||||||
|
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
|
import "../../../components/chips/ha-assist-chip";
|
||||||
import type {
|
import type {
|
||||||
DataTableColumnContainer,
|
DataTableColumnContainer,
|
||||||
RowClickedEvent,
|
RowClickedEvent,
|
||||||
} from "../../../components/data-table/ha-data-table";
|
} from "../../../components/data-table/ha-data-table";
|
||||||
import "../../../components/ha-button-related-filter-menu";
|
import "../../../components/entity/ha-entity-toggle";
|
||||||
import "../../../components/ha-label";
|
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
|
import "../../../components/ha-filter-floor-areas";
|
||||||
|
import "../../../components/ha-filter-blueprints";
|
||||||
|
import "../../../components/ha-filter-categories";
|
||||||
|
import "../../../components/ha-filter-devices";
|
||||||
|
import "../../../components/ha-filter-entities";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-icon-overflow-menu";
|
import "../../../components/ha-icon-overflow-menu";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
@ -49,28 +57,43 @@ import {
|
|||||||
showAutomationEditor,
|
showAutomationEditor,
|
||||||
triggerAutomationActions,
|
triggerAutomationActions,
|
||||||
} from "../../../data/automation";
|
} from "../../../data/automation";
|
||||||
|
import {
|
||||||
|
CategoryRegistryEntry,
|
||||||
|
subscribeCategoryRegistry,
|
||||||
|
} from "../../../data/category_registry";
|
||||||
|
import { fullEntitiesContext } from "../../../data/context";
|
||||||
|
import { UNAVAILABLE } from "../../../data/entity";
|
||||||
|
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||||
|
import { findRelated } from "../../../data/search";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
} from "../../../dialogs/generic/show-dialog-box";
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import { HomeAssistant, Route } from "../../../types";
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
|
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||||
import { configSections } from "../ha-panel-config";
|
import { configSections } from "../ha-panel-config";
|
||||||
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
||||||
import { findRelated } from "../../../data/search";
|
import "../../../components/data-table/ha-data-table-labels";
|
||||||
import { fetchBlueprints } from "../../../data/blueprint";
|
import {
|
||||||
import { UNAVAILABLE } from "../../../data/entity";
|
LabelRegistryEntry,
|
||||||
|
subscribeLabelRegistry,
|
||||||
|
} from "../../../data/label_registry";
|
||||||
|
import "../../../components/ha-filter-labels";
|
||||||
|
|
||||||
type AutomationItem = AutomationEntity & {
|
type AutomationItem = AutomationEntity & {
|
||||||
name: string;
|
name: string;
|
||||||
last_triggered?: string | undefined;
|
last_triggered?: string | undefined;
|
||||||
disabled: boolean;
|
formatted_state: string;
|
||||||
|
category: string | undefined;
|
||||||
|
labels: LabelRegistryEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@customElement("ha-automation-picker")
|
@customElement("ha-automation-picker")
|
||||||
class HaAutomationPicker extends LitElement {
|
class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean }) public isWide = false;
|
@property({ type: Boolean }) public isWide = false;
|
||||||
@ -81,17 +104,33 @@ class HaAutomationPicker extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public automations!: AutomationEntity[];
|
@property({ attribute: false }) public automations!: AutomationEntity[];
|
||||||
|
|
||||||
@state() private _activeFilters?: string[];
|
|
||||||
|
|
||||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
@state() private _filteredAutomations?: string[] | null;
|
@state() private _filteredAutomations?: string[] | null;
|
||||||
|
|
||||||
@state() private _filterValue?;
|
@state() private _filters: Record<
|
||||||
|
string,
|
||||||
|
{ value: string[] | undefined; items: Set<string> | undefined }
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
@state() private _expandedFilter?: string;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
_categories!: CategoryRegistryEntry[];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
_labels!: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||||
|
_entityReg!: EntityRegistryEntry[];
|
||||||
|
|
||||||
private _automations = memoizeOne(
|
private _automations = memoizeOne(
|
||||||
(
|
(
|
||||||
automations: AutomationEntity[],
|
automations: AutomationEntity[],
|
||||||
|
entityReg: EntityRegistryEntry[],
|
||||||
|
categoryReg?: CategoryRegistryEntry[],
|
||||||
|
labelReg?: LabelRegistryEntry[],
|
||||||
filteredAutomations?: string[] | null
|
filteredAutomations?: string[] | null
|
||||||
): AutomationItem[] => {
|
): AutomationItem[] => {
|
||||||
if (filteredAutomations === null) {
|
if (filteredAutomations === null) {
|
||||||
@ -103,23 +142,38 @@ class HaAutomationPicker extends LitElement {
|
|||||||
filteredAutomations!.includes(automation.entity_id)
|
filteredAutomations!.includes(automation.entity_id)
|
||||||
)
|
)
|
||||||
: automations
|
: automations
|
||||||
).map((automation) => ({
|
).map((automation) => {
|
||||||
...automation,
|
const entityRegEntry = entityReg.find(
|
||||||
name: computeStateName(automation),
|
(reg) => reg.entity_id === automation.entity_id
|
||||||
last_triggered: automation.attributes.last_triggered || undefined,
|
);
|
||||||
disabled: automation.state === "off",
|
const category = entityRegEntry?.categories.automation;
|
||||||
}));
|
const labels = labelReg && entityRegEntry?.labels;
|
||||||
|
return {
|
||||||
|
...automation,
|
||||||
|
name: computeStateName(automation),
|
||||||
|
last_triggered: automation.attributes.last_triggered || undefined,
|
||||||
|
formatted_state: this.hass.formatEntityState(automation),
|
||||||
|
category: category
|
||||||
|
? categoryReg?.find((cat) => cat.category_id === category)?.name
|
||||||
|
: undefined,
|
||||||
|
labels: (labels || []).map(
|
||||||
|
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private _columns = memoizeOne(
|
private _columns = memoizeOne(
|
||||||
(narrow: boolean, _locale): DataTableColumnContainer => {
|
(
|
||||||
|
narrow: boolean,
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
locale: HomeAssistant["locale"]
|
||||||
|
): DataTableColumnContainer => {
|
||||||
const columns: DataTableColumnContainer<AutomationItem> = {
|
const columns: DataTableColumnContainer<AutomationItem> = {
|
||||||
icon: {
|
icon: {
|
||||||
title: "",
|
title: "",
|
||||||
label: this.hass.localize(
|
label: localize("ui.panel.config.automation.picker.headers.state"),
|
||||||
"ui.panel.config.automation.picker.headers.state"
|
|
||||||
),
|
|
||||||
type: "icon",
|
type: "icon",
|
||||||
template: (automation) =>
|
template: (automation) =>
|
||||||
html`<ha-state-icon
|
html`<ha-state-icon
|
||||||
@ -134,95 +188,91 @@ class HaAutomationPicker extends LitElement {
|
|||||||
></ha-state-icon>`,
|
></ha-state-icon>`,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
title: this.hass.localize(
|
title: localize("ui.panel.config.automation.picker.headers.name"),
|
||||||
"ui.panel.config.automation.picker.headers.name"
|
|
||||||
),
|
|
||||||
main: true,
|
main: true,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
grows: true,
|
grows: true,
|
||||||
template: narrow
|
|
||||||
? (automation) => {
|
|
||||||
const date = new Date(automation.attributes.last_triggered);
|
|
||||||
const now = new Date();
|
|
||||||
const dayDifference = differenceInDays(now, date);
|
|
||||||
return html`
|
|
||||||
${automation.name}
|
|
||||||
<div class="secondary">
|
|
||||||
${this.hass.localize("ui.card.automation.last_triggered")}:
|
|
||||||
${automation.attributes.last_triggered
|
|
||||||
? dayDifference > 3
|
|
||||||
? formatShortDateTime(
|
|
||||||
date,
|
|
||||||
this.hass.locale,
|
|
||||||
this.hass.config
|
|
||||||
)
|
|
||||||
: relativeTime(date, this.hass.locale)
|
|
||||||
: this.hass.localize("ui.components.relative_time.never")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (!narrow) {
|
|
||||||
columns.last_triggered = {
|
|
||||||
sortable: true,
|
|
||||||
width: "20%",
|
|
||||||
title: this.hass.localize("ui.card.automation.last_triggered"),
|
|
||||||
template: (automation) => {
|
template: (automation) => {
|
||||||
if (!automation.last_triggered) {
|
const date = new Date(automation.attributes.last_triggered);
|
||||||
return this.hass.localize("ui.components.relative_time.never");
|
|
||||||
}
|
|
||||||
const date = new Date(automation.last_triggered);
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const dayDifference = differenceInDays(now, date);
|
const dayDifference = differenceInDays(now, date);
|
||||||
return html`
|
return html`
|
||||||
${dayDifference > 3
|
<div style="font-size: 14px;">${automation.name}</div>
|
||||||
? formatShortDateTime(date, this.hass.locale, this.hass.config)
|
${narrow
|
||||||
: relativeTime(date, this.hass.locale)}
|
? 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}
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
title: localize("ui.panel.config.automation.picker.headers.category"),
|
||||||
|
hidden: true,
|
||||||
|
groupable: true,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
title: "",
|
||||||
|
hidden: true,
|
||||||
|
filterable: true,
|
||||||
|
template: (automation) =>
|
||||||
|
automation.labels.map((lbl) => lbl.name).join(" "),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
columns.last_triggered = {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
const date = new Date(automation.last_triggered);
|
||||||
|
const now = new Date();
|
||||||
|
const dayDifference = differenceInDays(now, date);
|
||||||
|
return html`
|
||||||
|
${dayDifference > 3
|
||||||
|
? formatShortDateTime(date, locale, this.hass.config)
|
||||||
|
: relativeTime(date, locale)}
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.narrow) {
|
||||||
|
columns.formatted_state = {
|
||||||
|
width: "82px",
|
||||||
|
sortable: true,
|
||||||
|
groupable: true,
|
||||||
|
title: "",
|
||||||
|
label: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||||
|
template: (automation) => html`
|
||||||
|
<ha-entity-toggle
|
||||||
|
.stateObj=${automation}
|
||||||
|
.hass=${this.hass}
|
||||||
|
></ha-entity-toggle>
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
columns.disabled = this.narrow
|
|
||||||
? {
|
|
||||||
title: "",
|
|
||||||
template: (automation) =>
|
|
||||||
automation.disabled
|
|
||||||
? html`
|
|
||||||
<simple-tooltip animation-delay="0" position="left">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.automation.picker.disabled"
|
|
||||||
)}
|
|
||||||
</simple-tooltip>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiCancel}
|
|
||||||
style="color: var(--secondary-text-color)"
|
|
||||||
></ha-svg-icon>
|
|
||||||
`
|
|
||||||
: "",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
width: "20%",
|
|
||||||
title: "",
|
|
||||||
template: (automation) =>
|
|
||||||
automation.disabled
|
|
||||||
? html`
|
|
||||||
<ha-label>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.automation.picker.disabled"
|
|
||||||
)}
|
|
||||||
</ha-label>
|
|
||||||
`
|
|
||||||
: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
columns.actions = {
|
columns.actions = {
|
||||||
title: "",
|
title: "",
|
||||||
width: this.narrow ? undefined : "10%",
|
width: "64px",
|
||||||
type: "overflow-menu",
|
type: "overflow-menu",
|
||||||
template: (automation) => html`
|
template: (automation) => html`
|
||||||
<ha-icon-overflow-menu
|
<ha-icon-overflow-menu
|
||||||
@ -236,6 +286,13 @@ class HaAutomationPicker extends LitElement {
|
|||||||
),
|
),
|
||||||
action: () => this._showInfo(automation),
|
action: () => this._showInfo(automation),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: mdiTag,
|
||||||
|
label: this.hass.localize(
|
||||||
|
`ui.panel.config.automation.picker.${automation.category ? "edit_category" : "assign_category"}`
|
||||||
|
),
|
||||||
|
action: () => this._editCategory(automation),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: mdiPlay,
|
path: mdiPlay,
|
||||||
label: this.hass.localize(
|
label: this.hass.localize(
|
||||||
@ -292,6 +349,21 @@ class HaAutomationPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeCategoryRegistry(
|
||||||
|
this.hass.connection,
|
||||||
|
"automation",
|
||||||
|
(categories) => {
|
||||||
|
this._categories = categories;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
this._labels = labels;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage-data-table
|
<hass-tabs-subpage-data-table
|
||||||
@ -301,9 +373,23 @@ class HaAutomationPicker extends LitElement {
|
|||||||
id="entity_id"
|
id="entity_id"
|
||||||
.route=${this.route}
|
.route=${this.route}
|
||||||
.tabs=${configSections.automations}
|
.tabs=${configSections.automations}
|
||||||
.activeFilters=${this._activeFilters}
|
hasFilters
|
||||||
.columns=${this._columns(this.narrow, this.hass.locale)}
|
.filters=${Object.values(this._filters).filter(
|
||||||
.data=${this._automations(this.automations, this._filteredAutomations)}
|
(filter) => filter.value?.length
|
||||||
|
).length}
|
||||||
|
.columns=${this._columns(
|
||||||
|
this.narrow,
|
||||||
|
this.hass.localize,
|
||||||
|
this.hass.locale
|
||||||
|
)}
|
||||||
|
initialGroupColumn="category"
|
||||||
|
.data=${this._automations(
|
||||||
|
this.automations,
|
||||||
|
this._entityReg,
|
||||||
|
this._categories,
|
||||||
|
this._labels,
|
||||||
|
this._filteredAutomations
|
||||||
|
)}
|
||||||
.empty=${!this.automations.length}
|
.empty=${!this.automations.length}
|
||||||
@row-click=${this._handleRowClicked}
|
@row-click=${this._handleRowClicked}
|
||||||
.noDataText=${this.hass.localize(
|
.noDataText=${this.hass.localize(
|
||||||
@ -312,6 +398,7 @@ class HaAutomationPicker extends LitElement {
|
|||||||
@clear-filter=${this._clearFilter}
|
@clear-filter=${this._clearFilter}
|
||||||
hasFab
|
hasFab
|
||||||
clickable
|
clickable
|
||||||
|
class=${this.narrow ? "narrow" : ""}
|
||||||
>
|
>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="toolbar-icon"
|
slot="toolbar-icon"
|
||||||
@ -319,15 +406,65 @@ class HaAutomationPicker extends LitElement {
|
|||||||
.path=${mdiHelpCircle}
|
.path=${mdiHelpCircle}
|
||||||
@click=${this._showHelp}
|
@click=${this._showHelp}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-button-related-filter-menu
|
<ha-filter-floor-areas
|
||||||
slot="filter-menu"
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${this._filterValue}
|
.type=${"automation"}
|
||||||
exclude-domains='["automation"]'
|
.value=${this._filters["ha-filter-floor-areas"]?.value}
|
||||||
@related-changed=${this._relatedFilterChanged}
|
@data-table-filter-changed=${this._filterChanged}
|
||||||
>
|
slot="filter-pane"
|
||||||
</ha-button-related-filter-menu>
|
.expanded=${this._expandedFilter === "ha-filter-floor-areas"}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
@expanded-changed=${this._filterExpanded}
|
||||||
|
></ha-filter-floor-areas>
|
||||||
|
<ha-filter-devices
|
||||||
|
.hass=${this.hass}
|
||||||
|
.type=${"automation"}
|
||||||
|
.value=${this._filters["ha-filter-devices"]?.value}
|
||||||
|
@data-table-filter-changed=${this._filterChanged}
|
||||||
|
slot="filter-pane"
|
||||||
|
.expanded=${this._expandedFilter === "ha-filter-devices"}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
@expanded-changed=${this._filterExpanded}
|
||||||
|
></ha-filter-devices>
|
||||||
|
<ha-filter-entities
|
||||||
|
.hass=${this.hass}
|
||||||
|
.type=${"automation"}
|
||||||
|
.value=${this._filters["ha-filter-entities"]?.value}
|
||||||
|
@data-table-filter-changed=${this._filterChanged}
|
||||||
|
slot="filter-pane"
|
||||||
|
.expanded=${this._expandedFilter === "ha-filter-entities"}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
@expanded-changed=${this._filterExpanded}
|
||||||
|
></ha-filter-entities>
|
||||||
|
<ha-filter-labels
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._filters["ha-filter-labels"]?.value}
|
||||||
|
@data-table-filter-changed=${this._filterChanged}
|
||||||
|
slot="filter-pane"
|
||||||
|
.expanded=${this._expandedFilter === "ha-filter-labels"}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
@expanded-changed=${this._filterExpanded}
|
||||||
|
></ha-filter-labels>
|
||||||
|
<ha-filter-categories
|
||||||
|
.hass=${this.hass}
|
||||||
|
scope="automation"
|
||||||
|
.value=${this._filters["ha-filter-categories"]?.value}
|
||||||
|
@data-table-filter-changed=${this._filterChanged}
|
||||||
|
slot="filter-pane"
|
||||||
|
.expanded=${this._expandedFilter === "ha-filter-categories"}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
@expanded-changed=${this._filterExpanded}
|
||||||
|
></ha-filter-categories>
|
||||||
|
<ha-filter-blueprints
|
||||||
|
.hass=${this.hass}
|
||||||
|
.type=${"automation"}
|
||||||
|
.value=${this._filters["ha-filter-blueprints"]?.value}
|
||||||
|
@data-table-filter-changed=${this._filterChanged}
|
||||||
|
slot="filter-pane"
|
||||||
|
.expanded=${this._expandedFilter === "ha-filter-blueprints"}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
@expanded-changed=${this._filterExpanded}
|
||||||
|
></ha-filter-blueprints>
|
||||||
${!this.automations.length
|
${!this.automations.length
|
||||||
? html`<div class="empty" slot="empty">
|
? html`<div class="empty" slot="empty">
|
||||||
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
|
||||||
@ -378,44 +515,114 @@ class HaAutomationPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _filterExpanded(ev) {
|
||||||
|
if (ev.detail.expanded) {
|
||||||
|
this._expandedFilter = ev.target.localName;
|
||||||
|
} else if (this._expandedFilter === ev.target.localName) {
|
||||||
|
this._expandedFilter = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _labelClicked = (ev: CustomEvent) => {
|
||||||
|
const label = ev.detail.label;
|
||||||
|
this._filters = {
|
||||||
|
...this._filters,
|
||||||
|
"ha-filter-labels": {
|
||||||
|
value: [label.label_id],
|
||||||
|
items: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this._applyFilters();
|
||||||
|
};
|
||||||
|
|
||||||
|
private _filterChanged(ev) {
|
||||||
|
const type = ev.target.localName;
|
||||||
|
this._filters[type] = ev.detail;
|
||||||
|
this._applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _applyFilters() {
|
||||||
|
const filters = Object.entries(this._filters);
|
||||||
|
let items: Set<string> | undefined;
|
||||||
|
for (const [key, filter] of filters) {
|
||||||
|
if (filter.items) {
|
||||||
|
if (!items) {
|
||||||
|
items = filter.items;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
items =
|
||||||
|
"intersection" in items
|
||||||
|
? // @ts-ignore
|
||||||
|
items.intersection(filter.items)
|
||||||
|
: new Set([...items].filter((x) => filter.items!.has(x)));
|
||||||
|
}
|
||||||
|
if (key === "ha-filter-categories" && filter.value?.length) {
|
||||||
|
const categoryItems: Set<string> = new Set();
|
||||||
|
this.automations
|
||||||
|
.filter(
|
||||||
|
(automation) =>
|
||||||
|
filter.value![0] ===
|
||||||
|
this._entityReg.find(
|
||||||
|
(reg) => reg.entity_id === automation.entity_id
|
||||||
|
)?.categories.automation
|
||||||
|
)
|
||||||
|
.forEach((automation) => categoryItems.add(automation.entity_id));
|
||||||
|
if (!items) {
|
||||||
|
items = categoryItems;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
items =
|
||||||
|
"intersection" in items
|
||||||
|
? // @ts-ignore
|
||||||
|
items.intersection(categoryItems)
|
||||||
|
: new Set([...items].filter((x) => categoryItems!.has(x)));
|
||||||
|
}
|
||||||
|
if (key === "ha-filter-labels" && filter.value?.length) {
|
||||||
|
const labelItems: Set<string> = new Set();
|
||||||
|
this.automations
|
||||||
|
.filter((automation) =>
|
||||||
|
this._entityReg
|
||||||
|
.find((reg) => reg.entity_id === automation.entity_id)
|
||||||
|
?.labels.some((lbl) => filter.value!.includes(lbl))
|
||||||
|
)
|
||||||
|
.forEach((automation) => labelItems.add(automation.entity_id));
|
||||||
|
if (!items) {
|
||||||
|
items = labelItems;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
items =
|
||||||
|
"intersection" in items
|
||||||
|
? // @ts-ignore
|
||||||
|
items.intersection(labelItems)
|
||||||
|
: new Set([...items].filter((x) => labelItems!.has(x)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._filteredAutomations = items ? [...items] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private async _filterBlueprint() {
|
private async _filterBlueprint() {
|
||||||
const blueprint = this._searchParms.get("blueprint");
|
const blueprint = this._searchParms.get("blueprint");
|
||||||
if (!blueprint) {
|
if (!blueprint) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [related, blueprints] = await Promise.all([
|
const related = await findRelated(
|
||||||
findRelated(this.hass, "automation_blueprint", blueprint),
|
this.hass,
|
||||||
fetchBlueprints(this.hass, "automation"),
|
"automation_blueprint",
|
||||||
]);
|
blueprint
|
||||||
this._filteredAutomations = related.automation || [];
|
);
|
||||||
const blueprintMeta = blueprints[blueprint];
|
this._filters = {
|
||||||
this._activeFilters = [
|
...this._filters,
|
||||||
this.hass.localize(
|
"ha-filter-blueprints": {
|
||||||
"ui.panel.config.automation.picker.filtered_by_blueprint",
|
value: [blueprint],
|
||||||
{
|
items: new Set(related.automation || []),
|
||||||
name:
|
},
|
||||||
!blueprintMeta || "error" in blueprintMeta
|
};
|
||||||
? blueprint
|
this._applyFilters();
|
||||||
: blueprintMeta.metadata.name || blueprint,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _relatedFilterChanged(ev: CustomEvent) {
|
|
||||||
this._filterValue = ev.detail.value;
|
|
||||||
if (!this._filterValue) {
|
|
||||||
this._clearFilter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._activeFilters = [ev.detail.filter];
|
|
||||||
this._filteredAutomations = ev.detail.items.automation || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearFilter() {
|
private _clearFilter() {
|
||||||
this._filteredAutomations = undefined;
|
this._filters = {};
|
||||||
this._activeFilters = undefined;
|
this._applyFilters();
|
||||||
this._filterValue = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showInfo(automation: any) {
|
private _showInfo(automation: any) {
|
||||||
@ -426,6 +633,27 @@ class HaAutomationPicker extends LitElement {
|
|||||||
triggerAutomationActions(this.hass, automation.entity_id);
|
triggerAutomationActions(this.hass, automation.entity_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _editCategory(automation: any) {
|
||||||
|
const entityReg = this._entityReg.find(
|
||||||
|
(reg) => reg.entity_id === automation.entity_id
|
||||||
|
);
|
||||||
|
if (!entityReg) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.picker.no_category_support"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.automation.picker.no_category_entity_reg"
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showAssignCategoryDialog(this, {
|
||||||
|
scope: "automation",
|
||||||
|
entityReg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _showTrace(automation: any) {
|
private _showTrace(automation: any) {
|
||||||
if (!automation.attributes.id) {
|
if (!automation.attributes.id) {
|
||||||
showAlertDialog(this, {
|
showAlertDialog(this, {
|
||||||
@ -552,6 +780,12 @@ class HaAutomationPicker extends LitElement {
|
|||||||
return [
|
return [
|
||||||
haStyle,
|
haStyle,
|
||||||
css`
|
css`
|
||||||
|
hass-tabs-subpage-data-table {
|
||||||
|
--data-table-row-height: 60px;
|
||||||
|
}
|
||||||
|
hass-tabs-subpage-data-table.narrow {
|
||||||
|
--data-table-row-height: 72px;
|
||||||
|
}
|
||||||
.empty {
|
.empty {
|
||||||
--paper-font-headline_-_font-size: 28px;
|
--paper-font-headline_-_font-size: 28px;
|
||||||
--mdc-icon-size: 80px;
|
--mdc-icon-size: 80px;
|
||||||
|
@ -261,7 +261,7 @@ class HaBlueprintOverview extends LitElement {
|
|||||||
hasFab
|
hasFab
|
||||||
clickable
|
clickable
|
||||||
@row-click=${this._handleRowClicked}
|
@row-click=${this._handleRowClicked}
|
||||||
.appendRow=${html` <div
|
.appendRow=${html`<div
|
||||||
class="mdc-data-table__cell"
|
class="mdc-data-table__cell"
|
||||||
style="width: 100%; text-align: center;"
|
style="width: 100%; text-align: center;"
|
||||||
role="cell"
|
role="cell"
|
||||||
|
132
src/panels/config/category/dialog-assign-category.ts
Normal file
132
src/panels/config/category/dialog-assign-category.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
|
import "../../../components/ha-icon-picker";
|
||||||
|
import "../../../components/ha-settings-row";
|
||||||
|
import "../../../components/ha-textfield";
|
||||||
|
import { updateEntityRegistryEntry } from "../../../data/entity_registry";
|
||||||
|
import { haStyleDialog } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import "./ha-category-picker";
|
||||||
|
import { AssignCategoryDialogParams } from "./show-dialog-assign-category";
|
||||||
|
|
||||||
|
@customElement("dialog-assign-category")
|
||||||
|
class DialogAssignCategory extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _scope?: string;
|
||||||
|
|
||||||
|
@state() private _category?: string;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _params?: AssignCategoryDialogParams;
|
||||||
|
|
||||||
|
@state() private _submitting?: boolean;
|
||||||
|
|
||||||
|
public showDialog(params: AssignCategoryDialogParams): void {
|
||||||
|
this._params = params;
|
||||||
|
this._scope = params.scope;
|
||||||
|
this._category = params.entityReg.categories[params.scope];
|
||||||
|
this._error = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._error = "";
|
||||||
|
this._params = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
const entry = this._params.entityReg.categories[this._params.scope];
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
.heading=${createCloseHeading(
|
||||||
|
this.hass,
|
||||||
|
entry
|
||||||
|
? this.hass.localize("ui.panel.config.category.assign.edit")
|
||||||
|
: this.hass.localize("ui.panel.config.category.assign.assign")
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
<div class="form">
|
||||||
|
<ha-category-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.scope=${this._scope}
|
||||||
|
.value=${this._category}
|
||||||
|
@value-changed=${this._categoryChanged}
|
||||||
|
></ha-category-picker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||||
|
${this.hass.localize("ui.common.cancel")}
|
||||||
|
</mwc-button>
|
||||||
|
<mwc-button
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this._updateEntry}
|
||||||
|
.disabled=${this._submitting}
|
||||||
|
>
|
||||||
|
${this.hass.localize("ui.common.save")}
|
||||||
|
</mwc-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _categoryChanged(ev: CustomEvent): void {
|
||||||
|
if (!ev.detail.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._category = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateEntry() {
|
||||||
|
this._submitting = true;
|
||||||
|
this._error = undefined;
|
||||||
|
try {
|
||||||
|
await updateEntityRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
this._params!.entityReg.entity_id,
|
||||||
|
{
|
||||||
|
categories: { [this._scope!]: this._category || null },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.closeDialog();
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error =
|
||||||
|
err.message ||
|
||||||
|
this.hass.localize("ui.panel.config.category.assign.unknown_error");
|
||||||
|
} finally {
|
||||||
|
this._submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-textfield,
|
||||||
|
ha-icon-picker {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-assign-category": DialogAssignCategory;
|
||||||
|
}
|
||||||
|
}
|
175
src/panels/config/category/dialog-category-registry-detail.ts
Normal file
175
src/panels/config/category/dialog-category-registry-detail.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
|
import "../../../components/ha-icon-picker";
|
||||||
|
import "../../../components/ha-settings-row";
|
||||||
|
import "../../../components/ha-textfield";
|
||||||
|
import {
|
||||||
|
CategoryRegistryEntryMutableParams,
|
||||||
|
createCategoryRegistryEntry,
|
||||||
|
updateCategoryRegistryEntry,
|
||||||
|
} from "../../../data/category_registry";
|
||||||
|
import { haStyleDialog } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { CategoryRegistryDetailDialogParams } from "./show-dialog-category-registry-detail";
|
||||||
|
|
||||||
|
class DialogCategoryDetail extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _name!: string;
|
||||||
|
|
||||||
|
@state() private _icon!: string | null;
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
@state() private _params?: CategoryRegistryDetailDialogParams;
|
||||||
|
|
||||||
|
@state() private _submitting?: boolean;
|
||||||
|
|
||||||
|
public async showDialog(
|
||||||
|
params: CategoryRegistryDetailDialogParams
|
||||||
|
): Promise<void> {
|
||||||
|
this._params = params;
|
||||||
|
this._error = undefined;
|
||||||
|
this._name = this._params.entry ? this._params.entry.name : "";
|
||||||
|
this._icon = this._params.entry?.icon || null;
|
||||||
|
await this.updateComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._error = "";
|
||||||
|
this._params = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
const entry = this._params.entry;
|
||||||
|
const nameInvalid = !this._isNameValid();
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
.heading=${createCloseHeading(
|
||||||
|
this.hass,
|
||||||
|
entry
|
||||||
|
? this.hass.localize("ui.panel.config.category.editor.edit")
|
||||||
|
: this.hass.localize("ui.panel.config.category.editor.create")
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
${this._error
|
||||||
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
<div class="form">
|
||||||
|
<ha-textfield
|
||||||
|
.value=${this._name}
|
||||||
|
@input=${this._nameChanged}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.name"
|
||||||
|
)}
|
||||||
|
.validationMessage=${this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.required_error_msg"
|
||||||
|
)}
|
||||||
|
required
|
||||||
|
dialogInitialFocus
|
||||||
|
></ha-textfield>
|
||||||
|
|
||||||
|
<ha-icon-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${this._icon}
|
||||||
|
@value-changed=${this._iconChanged}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.category.editor.icon"
|
||||||
|
)}
|
||||||
|
></ha-icon-picker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||||
|
${this.hass.localize("ui.common.cancel")}
|
||||||
|
</mwc-button>
|
||||||
|
<mwc-button
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this._updateEntry}
|
||||||
|
.disabled=${nameInvalid || this._submitting}
|
||||||
|
>
|
||||||
|
${entry
|
||||||
|
? this.hass.localize("ui.common.save")
|
||||||
|
: this.hass.localize("ui.common.add")}
|
||||||
|
</mwc-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _isNameValid() {
|
||||||
|
return this._name.trim() !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _nameChanged(ev) {
|
||||||
|
this._error = undefined;
|
||||||
|
this._name = ev.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _iconChanged(ev) {
|
||||||
|
this._error = undefined;
|
||||||
|
this._icon = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateEntry() {
|
||||||
|
const create = !this._params!.entry;
|
||||||
|
this._submitting = true;
|
||||||
|
try {
|
||||||
|
const values: CategoryRegistryEntryMutableParams = {
|
||||||
|
name: this._name.trim(),
|
||||||
|
icon: this._icon || (create ? undefined : null),
|
||||||
|
};
|
||||||
|
if (create) {
|
||||||
|
await createCategoryRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
this._params!.scope,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await updateCategoryRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
this._params!.scope,
|
||||||
|
this._params!.entry!.category_id,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.closeDialog();
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error =
|
||||||
|
err.message ||
|
||||||
|
this.hass.localize("ui.panel.config.category.editor.unknown_error");
|
||||||
|
} finally {
|
||||||
|
this._submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-textfield,
|
||||||
|
ha-icon-picker {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-category-registry-detail": DialogCategoryDetail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("dialog-category-registry-detail", DialogCategoryDetail);
|
281
src/panels/config/category/ha-category-picker.ts
Normal file
281
src/panels/config/category/ha-category-picker.ts
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import { html, LitElement, nothing, PropertyValues } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
fuzzyFilterSort,
|
||||||
|
ScorableTextItem,
|
||||||
|
} from "../../../common/string/filter/sequence-matching";
|
||||||
|
import "../../../components/ha-combo-box";
|
||||||
|
import type { HaComboBox } from "../../../components/ha-combo-box";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-list-item";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import {
|
||||||
|
CategoryRegistryEntry,
|
||||||
|
createCategoryRegistryEntry,
|
||||||
|
subscribeCategoryRegistry,
|
||||||
|
} from "../../../data/category_registry";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showPromptDialog,
|
||||||
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||||
|
|
||||||
|
type ScorableCategoryRegistryEntry = ScorableTextItem & CategoryRegistryEntry;
|
||||||
|
|
||||||
|
const rowRenderer: ComboBoxLitRenderer<CategoryRegistryEntry> = (item) =>
|
||||||
|
html`<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
class=${classMap({ "add-new": item.category_id === "add_new" })}
|
||||||
|
>
|
||||||
|
${item.icon
|
||||||
|
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${item.name}
|
||||||
|
</ha-list-item>`;
|
||||||
|
|
||||||
|
@customElement("ha-category-picker")
|
||||||
|
export class HaCategoryPicker extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public scope?: string;
|
||||||
|
|
||||||
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@property() public value?: string;
|
||||||
|
|
||||||
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ type: Boolean, attribute: "no-add" })
|
||||||
|
public noAdd = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
|
@state() private _opened?: boolean;
|
||||||
|
|
||||||
|
@state() private _categories?: CategoryRegistryEntry[];
|
||||||
|
|
||||||
|
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||||
|
|
||||||
|
protected hassSubscribeRequiredHostProps = ["scope"];
|
||||||
|
|
||||||
|
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||||
|
return [
|
||||||
|
subscribeCategoryRegistry(
|
||||||
|
this.hass.connection,
|
||||||
|
this.scope!,
|
||||||
|
(categories) => {
|
||||||
|
this._categories = categories;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _suggestion?: string;
|
||||||
|
|
||||||
|
private _init = false;
|
||||||
|
|
||||||
|
public async open() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async focus() {
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getCategories = memoizeOne(
|
||||||
|
(
|
||||||
|
categories: CategoryRegistryEntry[] | undefined,
|
||||||
|
noAdd: this["noAdd"]
|
||||||
|
): CategoryRegistryEntry[] => {
|
||||||
|
const result = categories ? [...categories] : [];
|
||||||
|
if (!result?.length) {
|
||||||
|
result.push({
|
||||||
|
category_id: "no_categories",
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.components.category-picker.no_categories"
|
||||||
|
),
|
||||||
|
icon: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return noAdd
|
||||||
|
? result
|
||||||
|
: [
|
||||||
|
...result,
|
||||||
|
{
|
||||||
|
category_id: "add_new",
|
||||||
|
name: this.hass.localize("ui.components.category-picker.add_new"),
|
||||||
|
icon: "mdi:plus",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues) {
|
||||||
|
if (
|
||||||
|
(!this._init && this.hass && this._categories) ||
|
||||||
|
(this._init && changedProps.has("_opened") && this._opened)
|
||||||
|
) {
|
||||||
|
this._init = true;
|
||||||
|
const categories = this._getCategories(this._categories, this.noAdd);
|
||||||
|
this.comboBox.items = categories;
|
||||||
|
this.comboBox.filteredItems = categories;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._categories) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ha-combo-box
|
||||||
|
.hass=${this.hass}
|
||||||
|
.helper=${this.helper}
|
||||||
|
item-value-path="category_id"
|
||||||
|
item-id-path="category_id"
|
||||||
|
item-label-path="name"
|
||||||
|
.value=${this._value}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
.required=${this.required}
|
||||||
|
.label=${this.label === undefined && this.hass
|
||||||
|
? this.hass.localize("ui.components.category-picker.category")
|
||||||
|
: this.label}
|
||||||
|
.placeholder=${this.placeholder}
|
||||||
|
.renderer=${rowRenderer}
|
||||||
|
@filter-changed=${this._filterChanged}
|
||||||
|
@opened-changed=${this._openedChanged}
|
||||||
|
@value-changed=${this._categoryChanged}
|
||||||
|
>
|
||||||
|
</ha-combo-box>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
|
const target = ev.target as HaComboBox;
|
||||||
|
const filterString = ev.detail.value;
|
||||||
|
if (!filterString) {
|
||||||
|
this.comboBox.filteredItems = this.comboBox.items;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredItems = fuzzyFilterSort<ScorableCategoryRegistryEntry>(
|
||||||
|
filterString,
|
||||||
|
target.items || []
|
||||||
|
);
|
||||||
|
if (!this.noAdd && filteredItems?.length === 0) {
|
||||||
|
this._suggestion = filterString;
|
||||||
|
this.comboBox.filteredItems = [
|
||||||
|
{
|
||||||
|
category_id: "add_new_suggestion",
|
||||||
|
name: this.hass.localize(
|
||||||
|
"ui.components.category-picker.add_new_sugestion",
|
||||||
|
{ name: this._suggestion }
|
||||||
|
),
|
||||||
|
picture: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
this.comboBox.filteredItems = filteredItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get _value() {
|
||||||
|
return this.value || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||||
|
this._opened = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _categoryChanged(ev: ValueChangedEvent<string>) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
let newValue = ev.detail.value;
|
||||||
|
|
||||||
|
if (newValue === "no_categories") {
|
||||||
|
newValue = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["add_new_suggestion", "add_new"].includes(newValue)) {
|
||||||
|
if (newValue !== this._value) {
|
||||||
|
this._setValue(newValue);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(ev.target as any).value = this._value;
|
||||||
|
showPromptDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.components.category-picker.add_dialog.title"
|
||||||
|
),
|
||||||
|
text: this.hass.localize("ui.components.category-picker.add_dialog.text"),
|
||||||
|
confirmText: this.hass.localize(
|
||||||
|
"ui.components.category-picker.add_dialog.add"
|
||||||
|
),
|
||||||
|
inputLabel: this.hass.localize(
|
||||||
|
"ui.components.category-picker.add_dialog.name"
|
||||||
|
),
|
||||||
|
defaultValue:
|
||||||
|
newValue === "add_new_suggestion" ? this._suggestion : undefined,
|
||||||
|
confirm: async (name) => {
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const category = await createCategoryRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
this.scope!,
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._categories = [...this._categories!, category];
|
||||||
|
this.comboBox.filteredItems = this._getCategories(
|
||||||
|
this._categories,
|
||||||
|
this.noAdd
|
||||||
|
);
|
||||||
|
await this.updateComplete;
|
||||||
|
await this.comboBox.updateComplete;
|
||||||
|
this._setValue(category.category_id);
|
||||||
|
} catch (err: any) {
|
||||||
|
showAlertDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.components.category-picker.add_dialog.failed_create_category"
|
||||||
|
),
|
||||||
|
text: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel: () => {
|
||||||
|
this._setValue(undefined);
|
||||||
|
this._suggestion = undefined;
|
||||||
|
this.comboBox.setInputValue("");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setValue(value?: string) {
|
||||||
|
this.value = value;
|
||||||
|
setTimeout(() => {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
fireEvent(this, "change");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-category-picker": HaCategoryPicker;
|
||||||
|
}
|
||||||
|
}
|
21
src/panels/config/category/show-dialog-assign-category.ts
Normal file
21
src/panels/config/category/show-dialog-assign-category.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||||
|
|
||||||
|
export interface AssignCategoryDialogParams {
|
||||||
|
entityReg: EntityRegistryEntry;
|
||||||
|
scope: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAssignCategoryDialog = () =>
|
||||||
|
import("./dialog-assign-category");
|
||||||
|
|
||||||
|
export const showAssignCategoryDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: AssignCategoryDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-assign-category",
|
||||||
|
dialogImport: loadAssignCategoryDialog,
|
||||||
|
dialogParams,
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,21 @@
|
|||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { CategoryRegistryEntry } from "../../../data/category_registry";
|
||||||
|
|
||||||
|
export interface CategoryRegistryDetailDialogParams {
|
||||||
|
entry?: CategoryRegistryEntry;
|
||||||
|
scope: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadCategoryRegistryDetailDialog = () =>
|
||||||
|
import("./dialog-category-registry-detail");
|
||||||
|
|
||||||
|
export const showCategoryRegistryDetailDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: CategoryRegistryDetailDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-category-registry-detail",
|
||||||
|
dialogImport: loadCategoryRegistryDetailDialog,
|
||||||
|
dialogParams,
|
||||||
|
});
|
||||||
|
};
|
@ -737,6 +737,7 @@ export class HaConfigEntities extends LitElement {
|
|||||||
has_entity_name: false,
|
has_entity_name: false,
|
||||||
options: null,
|
options: null,
|
||||||
labels: [],
|
labels: [],
|
||||||
|
categories: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
@ -166,6 +166,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
width: "25%",
|
width: "25%",
|
||||||
filterable: true,
|
filterable: true,
|
||||||
|
groupable: true,
|
||||||
};
|
};
|
||||||
columns.editable = {
|
columns.editable = {
|
||||||
title: "",
|
title: "",
|
||||||
|
@ -70,6 +70,7 @@ import "./ha-integration-overflow-menu";
|
|||||||
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
|
||||||
import "./ha-disabled-config-entry-card";
|
import "./ha-disabled-config-entry-card";
|
||||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||||
|
import "../../../components/search-input-outlined";
|
||||||
|
|
||||||
export interface ConfigEntryExtended extends ConfigEntry {
|
export interface ConfigEntryExtended extends ConfigEntry {
|
||||||
localized_domain_name?: string;
|
localized_domain_name?: string;
|
||||||
@ -327,15 +328,16 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
|
|||||||
${this.narrow
|
${this.narrow
|
||||||
? html`
|
? html`
|
||||||
<div slot="header">
|
<div slot="header">
|
||||||
<search-input
|
<search-input-outlined
|
||||||
|
class="header"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.filter=${this._filter}
|
.filter=${this._filter}
|
||||||
class="header"
|
|
||||||
@value-changed=${this._handleSearchChange}
|
@value-changed=${this._handleSearchChange}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.integrations.search"
|
"ui.panel.config.integrations.search"
|
||||||
)}
|
)}
|
||||||
></search-input>
|
>
|
||||||
|
</search-input-outlined>
|
||||||
</div>
|
</div>
|
||||||
${filterMenu}
|
${filterMenu}
|
||||||
`
|
`
|
||||||
@ -345,36 +347,36 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
|
|||||||
slot="toolbar-icon"
|
slot="toolbar-icon"
|
||||||
></ha-integration-overflow-menu>
|
></ha-integration-overflow-menu>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<search-input
|
<search-input-outlined
|
||||||
|
class="header"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
suffix
|
|
||||||
.filter=${this._filter}
|
.filter=${this._filter}
|
||||||
@value-changed=${this._handleSearchChange}
|
@value-changed=${this._handleSearchChange}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.integrations.search"
|
"ui.panel.config.integrations.search"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="filters" slot="suffix">
|
</search-input-outlined>
|
||||||
${!this._showDisabled && disabledConfigEntries.length
|
<div class="filters">
|
||||||
? html`<div
|
${!this._showDisabled && disabledConfigEntries.length
|
||||||
class="active-filters"
|
? html`<div
|
||||||
@click=${this._preventDefault}
|
class="active-filters"
|
||||||
>
|
@click=${this._preventDefault}
|
||||||
${this.hass.localize(
|
>
|
||||||
"ui.panel.config.integrations.disable.disabled_integrations",
|
${this.hass.localize(
|
||||||
{ number: disabledConfigEntries.length }
|
"ui.panel.config.integrations.disable.disabled_integrations",
|
||||||
|
{ number: disabledConfigEntries.length }
|
||||||
|
)}
|
||||||
|
<mwc-button
|
||||||
|
@click=${this._toggleShowDisabled}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.integrations.disable.show"
|
||||||
)}
|
)}
|
||||||
<mwc-button
|
></mwc-button>
|
||||||
@click=${this._toggleShowDisabled}
|
</div>`
|
||||||
.label=${this.hass.localize(
|
: ""}
|
||||||
"ui.panel.config.integrations.disable.show"
|
${filterMenu}
|
||||||
)}
|
</div>
|
||||||
></mwc-button>
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
${filterMenu}
|
|
||||||
</div>
|
|
||||||
</search-input>
|
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
${this._showIgnored
|
${this._showIgnored
|
||||||
@ -810,36 +812,23 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
|
|||||||
.empty-message h1 {
|
.empty-message h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
search-input {
|
search-input-outlined {
|
||||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
flex: 1;
|
||||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
|
||||||
--text-field-overflow: visible;
|
|
||||||
}
|
|
||||||
search-input.header {
|
|
||||||
display: block;
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
margin-left: 8px;
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
margin-inline-end: initial;
|
|
||||||
direction: var(--direction);
|
|
||||||
--mdc-ripple-color: transparant;
|
|
||||||
}
|
}
|
||||||
.search {
|
.search {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
background-color: var(--primary-background-color);
|
||||||
.search search-input {
|
padding: 0 16px;
|
||||||
display: block;
|
gap: 16px;
|
||||||
position: absolute;
|
box-sizing: border-box;
|
||||||
top: 0;
|
border-bottom: 1px solid var(--divider-color);
|
||||||
right: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
}
|
||||||
.filters {
|
.filters {
|
||||||
--mdc-text-field-fill-color: var(--input-fill-color);
|
--mdc-text-field-fill-color: var(--input-fill-color);
|
||||||
@ -848,6 +837,7 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
|
|||||||
--text-field-overflow: initial;
|
--text-field-overflow: initial;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
.active-filters {
|
.active-filters {
|
||||||
@ -865,6 +855,7 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
|
|||||||
width: max-content;
|
width: max-content;
|
||||||
cursor: initial;
|
cursor: initial;
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
|
height: 32px;
|
||||||
}
|
}
|
||||||
.active-filters mwc-button {
|
.active-filters mwc-button {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@ -27,7 +27,6 @@ import {
|
|||||||
DataTableColumnContainer,
|
DataTableColumnContainer,
|
||||||
RowClickedEvent,
|
RowClickedEvent,
|
||||||
} from "../../../components/data-table/ha-data-table";
|
} from "../../../components/data-table/ha-data-table";
|
||||||
import "../../../components/ha-button-related-filter-menu";
|
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
import "../../../components/ha-button";
|
import "../../../components/ha-button";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
@ -76,8 +75,6 @@ class HaSceneDashboard extends LitElement {
|
|||||||
|
|
||||||
@state() private _filteredScenes?: string[] | null;
|
@state() private _filteredScenes?: string[] | null;
|
||||||
|
|
||||||
@state() private _filterValue?;
|
|
||||||
|
|
||||||
private _scenes = memoizeOne(
|
private _scenes = memoizeOne(
|
||||||
(scenes: SceneEntity[], filteredScenes?: string[] | null): SceneItem[] => {
|
(scenes: SceneEntity[], filteredScenes?: string[] | null): SceneItem[] => {
|
||||||
if (filteredScenes === null) {
|
if (filteredScenes === null) {
|
||||||
@ -242,15 +239,6 @@ class HaSceneDashboard extends LitElement {
|
|||||||
.label=${this.hass.localize("ui.common.help")}
|
.label=${this.hass.localize("ui.common.help")}
|
||||||
.path=${mdiHelpCircle}
|
.path=${mdiHelpCircle}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-button-related-filter-menu
|
|
||||||
slot="filter-menu"
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this._filterValue}
|
|
||||||
exclude-domains='["scene"]'
|
|
||||||
@related-changed=${this._relatedFilterChanged}
|
|
||||||
>
|
|
||||||
</ha-button-related-filter-menu>
|
|
||||||
${!this.scenes.length
|
${!this.scenes.length
|
||||||
? html`<div class="empty" slot="empty">
|
? html`<div class="empty" slot="empty">
|
||||||
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
|
||||||
@ -295,20 +283,9 @@ class HaSceneDashboard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _relatedFilterChanged(ev: CustomEvent) {
|
|
||||||
this._filterValue = ev.detail.value;
|
|
||||||
if (!this._filterValue) {
|
|
||||||
this._clearFilter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._activeFilters = [ev.detail.filter];
|
|
||||||
this._filteredScenes = ev.detail.items.scene || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearFilter() {
|
private _clearFilter() {
|
||||||
this._filteredScenes = undefined;
|
this._filteredScenes = undefined;
|
||||||
this._activeFilters = undefined;
|
this._activeFilters = undefined;
|
||||||
this._filterValue = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _showInfo(scene: SceneEntity) {
|
private _showInfo(scene: SceneEntity) {
|
||||||
|
@ -30,7 +30,6 @@ import {
|
|||||||
DataTableColumnContainer,
|
DataTableColumnContainer,
|
||||||
RowClickedEvent,
|
RowClickedEvent,
|
||||||
} from "../../../components/data-table/ha-data-table";
|
} from "../../../components/data-table/ha-data-table";
|
||||||
import "../../../components/ha-button-related-filter-menu";
|
|
||||||
import "../../../components/ha-fab";
|
import "../../../components/ha-fab";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-icon-overflow-menu";
|
import "../../../components/ha-icon-overflow-menu";
|
||||||
@ -83,8 +82,6 @@ class HaScriptPicker extends LitElement {
|
|||||||
|
|
||||||
@state() private _filteredScripts?: string[] | null;
|
@state() private _filteredScripts?: string[] | null;
|
||||||
|
|
||||||
@state() private _filterValue?;
|
|
||||||
|
|
||||||
private _scripts = memoizeOne(
|
private _scripts = memoizeOne(
|
||||||
(
|
(
|
||||||
scripts: ScriptEntity[],
|
scripts: ScriptEntity[],
|
||||||
@ -266,15 +263,6 @@ class HaScriptPicker extends LitElement {
|
|||||||
.path=${mdiHelpCircle}
|
.path=${mdiHelpCircle}
|
||||||
@click=${this._showHelp}
|
@click=${this._showHelp}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-button-related-filter-menu
|
|
||||||
slot="filter-menu"
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${this._filterValue}
|
|
||||||
exclude-domains='["script"]'
|
|
||||||
@related-changed=${this._relatedFilterChanged}
|
|
||||||
>
|
|
||||||
</ha-button-related-filter-menu>
|
|
||||||
${!this.scripts.length
|
${!this.scripts.length
|
||||||
? html` <div class="empty" slot="empty">
|
? html` <div class="empty" slot="empty">
|
||||||
<ha-svg-icon .path=${mdiScriptText}></ha-svg-icon>
|
<ha-svg-icon .path=${mdiScriptText}></ha-svg-icon>
|
||||||
@ -345,20 +333,9 @@ class HaScriptPicker extends LitElement {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _relatedFilterChanged(ev: CustomEvent) {
|
|
||||||
this._filterValue = ev.detail.value;
|
|
||||||
if (!this._filterValue) {
|
|
||||||
this._clearFilter();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._activeFilters = [ev.detail.filter];
|
|
||||||
this._filteredScripts = ev.detail.items.script || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearFilter() {
|
private _clearFilter() {
|
||||||
this._filteredScripts = undefined;
|
this._filteredScripts = undefined;
|
||||||
this._activeFilters = undefined;
|
this._activeFilters = undefined;
|
||||||
this._filterValue = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||||
|
@ -542,6 +542,7 @@ export class VoiceAssistantsExpose extends LitElement {
|
|||||||
)}
|
)}
|
||||||
.filter=${this._filter}
|
.filter=${this._filter}
|
||||||
selectable
|
selectable
|
||||||
|
.selected=${this._selectedEntities.length}
|
||||||
clickable
|
clickable
|
||||||
@selection-changed=${this._handleSelectionChanged}
|
@selection-changed=${this._handleSelectionChanged}
|
||||||
@clear-filter=${this._clearFilter}
|
@clear-filter=${this._clearFilter}
|
||||||
@ -559,12 +560,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
|||||||
})}
|
})}
|
||||||
slot="header"
|
slot="header"
|
||||||
>
|
>
|
||||||
<p class="selected-txt">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.entities.picker.selected",
|
|
||||||
{ number: this._selectedEntities.length }
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<div class="header-btns">
|
<div class="header-btns">
|
||||||
${!this.narrow
|
${!this.narrow
|
||||||
? html`
|
? html`
|
||||||
|
@ -499,6 +499,14 @@
|
|||||||
"add_entity_id": "Choose entity",
|
"add_entity_id": "Choose entity",
|
||||||
"add_label_id": "Choose label"
|
"add_label_id": "Choose label"
|
||||||
},
|
},
|
||||||
|
"subpage-data-table": {
|
||||||
|
"filters": "Filters",
|
||||||
|
"sort_by": "Sort by {sortColumn}",
|
||||||
|
"group_by": "Group by {groupColumn}",
|
||||||
|
"dont_group_by": "Don't group",
|
||||||
|
"select": "Select",
|
||||||
|
"selected": "Selected {selected}"
|
||||||
|
},
|
||||||
"config-entry-picker": {
|
"config-entry-picker": {
|
||||||
"config_entry": "Integration"
|
"config_entry": "Integration"
|
||||||
},
|
},
|
||||||
@ -547,6 +555,23 @@
|
|||||||
"device": "Device",
|
"device": "Device",
|
||||||
"no_area": "No area"
|
"no_area": "No area"
|
||||||
},
|
},
|
||||||
|
"category-picker": {
|
||||||
|
"clear": "Clear",
|
||||||
|
"show_categories": "Show categories",
|
||||||
|
"categories": "Categories",
|
||||||
|
"category": "Category",
|
||||||
|
"add_category": "Add category",
|
||||||
|
"add_new_sugestion": "Add new category ''{name}''",
|
||||||
|
"add_new": "Add new category…",
|
||||||
|
"no_categories": "You don't have any categories",
|
||||||
|
"add_dialog": {
|
||||||
|
"title": "Add new category",
|
||||||
|
"text": "Enter the name of the new category.",
|
||||||
|
"name": "Name",
|
||||||
|
"add": "Add",
|
||||||
|
"failed_create_category": "Failed to create category."
|
||||||
|
}
|
||||||
|
},
|
||||||
"label-picker": {
|
"label-picker": {
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"show_labels": "Show labels",
|
"show_labels": "Show labels",
|
||||||
@ -555,14 +580,7 @@
|
|||||||
"add_new_sugestion": "Add new label ''{name}''",
|
"add_new_sugestion": "Add new label ''{name}''",
|
||||||
"add_new": "Add new label…",
|
"add_new": "Add new label…",
|
||||||
"no_labels": "You don't have any labels",
|
"no_labels": "You don't have any labels",
|
||||||
"no_match": "No matching labels found",
|
"no_match": "No matching labels found"
|
||||||
"add_dialog": {
|
|
||||||
"title": "Add new label",
|
|
||||||
"text": "Enter the name of the new label.",
|
|
||||||
"name": "Name",
|
|
||||||
"add": "Add",
|
|
||||||
"failed_create_label": "Failed to create label."
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"area-picker": {
|
"area-picker": {
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
@ -1924,6 +1942,29 @@
|
|||||||
"aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor."
|
"aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"category": {
|
||||||
|
"caption": "Categories",
|
||||||
|
"assign": {
|
||||||
|
"edit": "Edit category",
|
||||||
|
"assign": "Assign category",
|
||||||
|
"unknown_error": "An unknown error happened when assigning the category"
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"edit": "Edit category",
|
||||||
|
"delete": "Delete category",
|
||||||
|
"add": "Add category",
|
||||||
|
"create": "Create category",
|
||||||
|
"name": "Name",
|
||||||
|
"icon": "Icon",
|
||||||
|
"required_error_msg": "[%key:ui::panel::config::zone::detail::required_error_msg%]",
|
||||||
|
"unknown_error": "An unknown error happened when saving the category",
|
||||||
|
"confirm_delete": "Are you sure you want to delete this category?",
|
||||||
|
"confirm_delete_text": "This will delete the category and unassign everything that is currently assigned to it."
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"show_all": "Show all"
|
||||||
|
}
|
||||||
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"caption": "Labels",
|
"caption": "Labels",
|
||||||
"description": "Group devices and entities",
|
"description": "Group devices and entities",
|
||||||
@ -2632,14 +2673,20 @@
|
|||||||
"delete_confirm_text": "{name} will be permanently deleted.",
|
"delete_confirm_text": "{name} will be permanently deleted.",
|
||||||
"duplicate": "[%key:ui::common::duplicate%]",
|
"duplicate": "[%key:ui::common::duplicate%]",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
|
"state": "State",
|
||||||
"filtered_by_blueprint": "blueprint: {name}",
|
"filtered_by_blueprint": "blueprint: {name}",
|
||||||
"traces_not_available": "[%key:ui::panel::config::automation::editor::traces_not_available%]",
|
"traces_not_available": "[%key:ui::panel::config::automation::editor::traces_not_available%]",
|
||||||
|
"edit_category": "Edit category",
|
||||||
|
"assign_category": "Assign category",
|
||||||
|
"no_category_support": "You can't assign an category to this automation",
|
||||||
|
"no_category_entity_reg": "To assign an category to an automation it needs to have a unique ID.",
|
||||||
"headers": {
|
"headers": {
|
||||||
"toggle": "Enable/disable",
|
"toggle": "Enable/disable",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"trigger": "Trigger",
|
"trigger": "Trigger",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"state": "State"
|
"state": "State",
|
||||||
|
"category": "Category"
|
||||||
},
|
},
|
||||||
"empty_header": "Start automating",
|
"empty_header": "Start automating",
|
||||||
"empty_text_1": "Automations make Home Assistant automatically respond to things happening in and around your home.",
|
"empty_text_1": "Automations make Home Assistant automatically respond to things happening in and around your home.",
|
||||||
@ -3965,6 +4012,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"restored": "Restored",
|
"restored": "Restored",
|
||||||
|
"available": "Available",
|
||||||
"unavailable": "Unavailable",
|
"unavailable": "Unavailable",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
"readonly": "Read-only",
|
"readonly": "Read-only",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user