From ccde9cceee631c8c9c6df603b078a869fdb6f9cd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 13:22:40 +0200 Subject: [PATCH] Add category and filters to helpers (#20346) * Add category and filters to helpers * Add support for adding label and category in multi select * remove labels multi --- src/data/search.ts | 1 + .../config/helpers/ha-config-helpers.ts | 519 +++++++++++++++--- src/translations/en.json | 3 +- 3 files changed, 460 insertions(+), 63 deletions(-) diff --git a/src/data/search.ts b/src/data/search.ts index ec21d03d58..5011f9a4c1 100644 --- a/src/data/search.ts +++ b/src/data/search.ts @@ -28,6 +28,7 @@ export type ItemType = | "entity" | "floor" | "group" + | "label" | "scene" | "script" | "automation_blueprint" diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index 125663863f..809f03a23e 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -1,5 +1,15 @@ +import { consume } from "@lit-labs/context"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; -import { mdiAlertCircle, mdiPencilOff, mdiPlus } from "@mdi/js"; +import { + mdiAlertCircle, + mdiChevronRight, + mdiCog, + mdiDotsVertical, + mdiMenuDown, + mdiPencilOff, + mdiPlus, + mdiTag, +} from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; import { CSSResultGroup, @@ -11,8 +21,9 @@ import { nothing, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { consume } from "@lit-labs/context"; import memoizeOne from "memoize-one"; +import { computeCssColor } from "../../../common/color/compute-color"; +import { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { navigate } from "../../../common/navigate"; import { @@ -23,22 +34,42 @@ import { extractSearchParam } from "../../../common/url/search-params"; import { DataTableColumnContainer, RowClickedEvent, + SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; import "../../../components/ha-fab"; +import "../../../components/ha-filter-categories"; +import "../../../components/ha-filter-devices"; +import "../../../components/ha-filter-entities"; +import "../../../components/ha-filter-floor-areas"; +import "../../../components/ha-filter-labels"; import "../../../components/ha-icon"; +import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-state-icon"; import "../../../components/ha-svg-icon"; +import { + CategoryRegistryEntry, + createCategoryRegistryEntry, + subscribeCategoryRegistry, +} from "../../../data/category_registry"; import { ConfigEntry, subscribeConfigEntries, } from "../../../data/config_entries"; import { getConfigFlowHandlers } from "../../../data/config_flow"; +import { fullEntitiesContext } from "../../../data/context"; import { EntityRegistryEntry, + UpdateEntityRegistryEntryResult, subscribeEntityRegistry, + updateEntityRegistryEntry, } from "../../../data/entity_registry"; import { domainToName } from "../../../data/integration"; +import { + LabelRegistryEntry, + createLabelRegistryEntry, + subscribeLabelRegistry, +} from "../../../data/label_registry"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { @@ -49,18 +80,15 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage-data-table"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; +import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { configSections } from "../ha-panel-config"; import "../integrations/ha-integration-overflow-menu"; import { isHelperDomain } from "./const"; import { showHelperDetailDialog } from "./show-dialog-helper-detail"; -import { - LabelRegistryEntry, - subscribeLabelRegistry, -} from "../../../data/label_registry"; -import { fullEntitiesContext } from "../../../data/context"; -import "../../../components/ha-filter-labels"; -import { haStyle } from "../../../resources/styles"; +import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; +import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; type HelperItem = { id: string; @@ -71,6 +99,7 @@ type HelperItem = { type: string; configEntry?: ConfigEntry; entity?: HassEntity; + category: string | undefined; label_entries: LabelRegistryEntry[]; }; @@ -111,6 +140,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { @state() private _configEntries?: Record; + @state() private _selected: string[] = []; + @state() private _activeFilters?: string[]; @state() private _filters: Record< @@ -120,6 +151,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { @state() private _expandedFilter?: string; + @state() + _categories!: CategoryRegistryEntry[]; + @state() _labels!: LabelRegistryEntry[]; @@ -156,65 +190,86 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { subscribeLabelRegistry(this.hass.connection, (labels) => { this._labels = labels; }), + subscribeCategoryRegistry( + this.hass.connection, + "helpers", + (categories) => { + this._categories = categories; + } + ), ]; } private _columns = memoizeOne( - (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => { - const columns: DataTableColumnContainer = { - icon: { - title: "", - label: localize("ui.panel.config.helpers.picker.headers.icon"), - type: "icon", - template: (helper) => - helper.entity - ? html`` - : html``, - }, - name: { - title: localize("ui.panel.config.helpers.picker.headers.name"), - main: true, - sortable: true, - filterable: true, - grows: true, - direction: "asc", - template: (helper) => html` -
${helper.name}
- ${narrow - ? html`
${helper.entity_id}
` - : nothing} - ${helper.label_entries.length - ? html` - - ` - : nothing} - `, - }, - }; - if (!narrow) { - columns.entity_id = { - title: localize("ui.panel.config.helpers.picker.headers.entity_id"), - sortable: true, - filterable: true, - width: "25%", - }; - } - columns.localized_type = { + ( + narrow: boolean, + localize: LocalizeFunc + ): DataTableColumnContainer => ({ + icon: { + title: "", + label: localize("ui.panel.config.helpers.picker.headers.icon"), + type: "icon", + template: (helper) => + helper.entity + ? html`` + : html``, + }, + name: { + title: localize("ui.panel.config.helpers.picker.headers.name"), + main: true, + sortable: true, + filterable: true, + grows: true, + direction: "asc", + template: (helper) => html` +
${helper.name}
+ ${narrow + ? html`
${helper.entity_id}
` + : nothing} + ${helper.label_entries.length + ? html` + + ` + : nothing} + `, + }, + entity_id: { + title: localize("ui.panel.config.helpers.picker.headers.entity_id"), + hidden: this.narrow, + sortable: true, + filterable: true, + width: "25%", + }, + category: { + title: localize("ui.panel.config.helpers.picker.headers.category"), + hidden: true, + groupable: true, + filterable: true, + sortable: true, + }, + labels: { + title: "", + hidden: true, + filterable: true, + template: (helper) => + helper.label_entries.map((lbl) => lbl.name).join(" "), + }, + localized_type: { title: localize("ui.panel.config.helpers.picker.headers.type"), sortable: true, width: "25%", filterable: true, groupable: true, - }; - columns.editable = { + }, + editable: { title: "", label: this.hass.localize( "ui.panel.config.helpers.picker.headers.editable" @@ -237,9 +292,36 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { ` : ""} `, - }; - return columns; - } + }, + actions: { + title: "", + width: "64px", + type: "overflow-menu", + template: (helper) => html` + this._openSettings(helper), + }, + { + path: mdiTag, + label: this.hass.localize( + `ui.panel.config.automation.picker.${helper.category ? "edit_category" : "assign_category"}` + ), + action: () => this._editCategory(helper), + }, + ]} + > + + `, + }, + }) ); private _getItems = memoizeOne( @@ -249,6 +331,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { entityEntries: Record, configEntries: Record, entityReg: EntityRegistryEntry[], + categoryReg?: CategoryRegistryEntry[], labelReg?: LabelRegistryEntry[], filteredStateItems?: string[] | null ): HelperItem[] => { @@ -305,6 +388,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { (reg) => reg.entity_id === item.entity_id ); const labels = labelReg && entityRegEntry?.labels; + const category = entityRegEntry?.categories.helpers; return { ...item, localized_type: item.configEntry @@ -315,6 +399,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { label_entries: (labels || []).map( (lbl) => labelReg!.find((label) => label.label_id === lbl)! ), + category: category + ? categoryReg?.find((cat) => cat.category_id === category)?.name + : undefined, }; }); } @@ -330,6 +417,66 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { return html` `; } + const categoryItems = html`${this._categories?.map( + (category) => + html` + ${category.icon + ? html`` + : html``} +
${category.name}
+
` + )} + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.no_category" + )} +
+
+ + +
+ ${this.hass.localize("ui.panel.config.category.editor.add")} +
+
`; + const labelItems = html`${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + return html` + + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + +
+ ${this.hass.localize("ui.panel.config.labels.add_label")} +
+
`; + return html` filter.value?.length @@ -348,9 +498,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { this._entityEntries, this._configEntries, this._entityReg, + this._categories, this._labels, this._filteredStateItems )} + initialGroupColumn="category" .activeFilters=${this._activeFilters} @clear-filter=${this._clearFilter} @row-click=${this._openEditDialog} @@ -361,6 +513,26 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { )} class=${this.narrow ? "narrow" : ""} > + + + + + ${!this.narrow + ? html` + + + + ${categoryItems} + + ${this.hass.dockedSidebar === "docked" + ? nothing + : html` + + + + ${labelItems} + `}` + : nothing} + ${this.narrow || this.hass.dockedSidebar === "docked" + ? html` + + ${ + this.narrow + ? html` + + ` + : html`` + } + + ${ + this.narrow + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.move_category" + )} +
+ +
+ ${categoryItems} +
` + : nothing + } + ${ + this.narrow || this.hass.dockedSidebar === "docked" + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} +
+ +
+ ${labelItems} +
` + : nothing + } +
` + : nothing} labelItems!.has(x))); } + if (key === "ha-filter-categories" && filter.value?.length) { + const categoryItems: Set = new Set(); + this._stateItems + .filter( + (stateItem) => + filter.value![0] === + this._entityReg.find( + (reg) => reg.entity_id === stateItem.entity_id + )?.categories.helpers + ) + .forEach((stateItem) => categoryItems.add(stateItem.entity_id)); + if (!items) { + items = categoryItems; + continue; + } + items = + "intersection" in items + ? // @ts-ignore + items.intersection(categoryItems) + : new Set([...items].filter((x) => categoryItems!.has(x))); + } } this._filteredStateItems = items ? [...items] : undefined; } @@ -446,6 +747,65 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { this._applyFilters(); } + private _editCategory(helper: any) { + const entityReg = this._entityReg.find( + (reg) => reg.entity_id === helper.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: "helpers", + entityReg, + }); + } + + private async _handleBulkCategory(ev) { + const category = ev.currentTarget.value; + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + categories: { helpers: category }, + }) + ); + }); + await Promise.all(promises); + } + + private async _handleBulkLabel(ev) { + const label = ev.currentTarget.value; + const action = ev.currentTarget.action; + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + labels: + action === "add" + ? this.hass.entities[entityId].labels.concat(label) + : this.hass.entities[entityId].labels.filter( + (lbl) => lbl !== label + ), + }) + ); + }); + await Promise.all(promises); + } + + private _handleSelectionChanged( + ev: HASSDomEvent + ): void { + this._selected = ev.detail.value; + } + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); if (this.route.path === "/add") { @@ -563,10 +923,35 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { } } + private _openSettings(helper: HelperItem) { + if (helper.entity) { + showMoreInfoDialog(this, { + entityId: helper.entity_id, + view: "settings", + }); + } else { + showOptionsFlowDialog(this, helper.configEntry!); + } + } + private _createHelper() { showHelperDetailDialog(this, {}); } + private _createCategory() { + showCategoryRegistryDetailDialog(this, { + scope: "helpers", + createEntry: (values) => + createCategoryRegistryEntry(this.hass, "helpers", values), + }); + } + + private _createLabel() { + showLabelDetailDialog(this, { + createEntry: (values) => createLabelRegistryEntry(this.hass, values), + }); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -577,6 +962,16 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { hass-tabs-subpage-data-table.narrow { --data-table-row-height: 72px; } + ha-assist-chip { + --ha-assist-chip-container-shape: 10px; + } + ha-button-menu-new ha-assist-chip { + --md-assist-chip-trailing-space: 8px; + } + ha-label { + --ha-label-background-color: var(--color, var(--grey-color)); + --ha-label-background-opacity: 0.5; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 90ccb308cd..ead12d761d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2263,7 +2263,8 @@ "name": "Name", "entity_id": "Entity ID", "type": "Type", - "editable": "Editable" + "editable": "Editable", + "category": "Category" }, "create_helper": "Create helper", "no_helpers": "Looks like you don't have any helpers yet!"