From 88c59c5c13a6dffed96cf6afd05e69c38e336fe3 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:30:24 +0100 Subject: [PATCH 01/14] Add label filter for helper page (#20281) * Label filter for helper page * Clean up debugging label --- .../config/helpers/ha-config-helpers.ts | 189 ++++++++++++++++-- 1 file changed, 174 insertions(+), 15 deletions(-) diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index 6be03d6771fd..125663863ff8 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -1,8 +1,17 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { mdiAlertCircle, mdiPencilOff, mdiPlus } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; -import { LitElement, PropertyValues, TemplateResult, html } from "lit"; +import { + CSSResultGroup, + LitElement, + PropertyValues, + TemplateResult, + css, + html, + nothing, +} from "lit"; import { customElement, property, state } from "lit/decorators"; +import { consume } from "@lit-labs/context"; import memoizeOne from "memoize-one"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { navigate } from "../../../common/navigate"; @@ -15,6 +24,7 @@ import { DataTableColumnContainer, RowClickedEvent, } from "../../../components/data-table/ha-data-table"; +import "../../../components/data-table/ha-data-table-labels"; import "../../../components/ha-fab"; import "../../../components/ha-icon"; import "../../../components/ha-state-icon"; @@ -44,6 +54,13 @@ 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"; type HelperItem = { id: string; @@ -54,6 +71,7 @@ type HelperItem = { type: string; configEntry?: ConfigEntry; entity?: HassEntity; + label_entries: LabelRegistryEntry[]; }; // This groups items by a key but only returns last entry per key. @@ -93,6 +111,24 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { @state() private _configEntries?: Record; + @state() private _activeFilters?: string[]; + + @state() private _filters: Record< + string, + { value: string[] | undefined; items: Set | undefined } + > = {}; + + @state() private _expandedFilter?: string; + + @state() + _labels!: LabelRegistryEntry[]; + + @state() + @consume({ context: fullEntitiesContext, subscribe: true }) + _entityReg!: EntityRegistryEntry[]; + + @state() private _filteredStateItems?: string[] | null; + public hassSubscribe() { return [ subscribeConfigEntries( @@ -117,6 +153,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { subscribeEntityRegistry(this.hass.connection!, (entries) => { this._entityEntries = groupByOne(entries, (entry) => entry.entity_id); }), + subscribeLabelRegistry(this.hass.connection, (labels) => { + this._labels = labels; + }), ]; } @@ -146,10 +185,17 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { grows: true, direction: "asc", template: (helper) => html` - ${helper.name} +
${helper.name}
${narrow ? html`
${helper.entity_id}
` - : ""} + : nothing} + ${helper.label_entries.length + ? html` + + ` + : nothing} `, }, }; @@ -201,8 +247,15 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { localize: LocalizeFunc, stateItems: HassEntity[], entityEntries: Record, - configEntries: Record + configEntries: Record, + entityReg: EntityRegistryEntry[], + labelReg?: LabelRegistryEntry[], + filteredStateItems?: string[] | null ): HelperItem[] => { + if (filteredStateItems === null) { + return []; + } + const configEntriesCopy = { ...configEntries }; const states = stateItems.map((entityState) => { @@ -241,14 +294,29 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { entity: undefined, })); - return [...states, ...entries].map((item) => ({ - ...item, - localized_type: item.configEntry - ? domainToName(localize, item.type) - : localize( - `ui.panel.config.helpers.types.${item.type}` as LocalizeKeys - ) || item.type, - })); + return [...states, ...entries] + .filter((item) => + filteredStateItems + ? filteredStateItems?.includes(item.entity_id) + : true + ) + .map((item) => { + const entityRegEntry = entityReg.find( + (reg) => reg.entity_id === item.entity_id + ); + const labels = labelReg && entityRegEntry?.labels; + return { + ...item, + localized_type: item.configEntry + ? domainToName(localize, item.type) + : localize( + `ui.panel.config.helpers.types.${item.type}` as LocalizeKeys + ) || item.type, + label_entries: (labels || []).map( + (lbl) => labelReg!.find((label) => label.label_id === lbl)! + ), + }; + }); } ); @@ -269,20 +337,40 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { back-path="/config" .route=${this.route} .tabs=${configSections.devices} + hasFilters + .filters=${Object.values(this._filters).filter( + (filter) => filter.value?.length + ).length} .columns=${this._columns(this.narrow, this.hass.localize)} .data=${this._getItems( this.hass.localize, this._stateItems, this._entityEntries, - this._configEntries + this._configEntries, + this._entityReg, + this._labels, + this._filteredStateItems )} + .activeFilters=${this._activeFilters} + @clear-filter=${this._clearFilter} @row-click=${this._openEditDialog} hasFab clickable .noDataText=${this.hass.localize( "ui.panel.config.helpers.picker.no_helpers" )} + class=${this.narrow ? "narrow" : ""} > + + @@ -301,6 +389,63 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { `; } + private _filterExpanded(ev) { + if (ev.detail.expanded) { + this._expandedFilter = ev.target.localName; + } else if (this._expandedFilter === ev.target.localName) { + this._expandedFilter = undefined; + } + } + + 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 | 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-labels" && filter.value?.length) { + const labelItems: Set = new Set(); + this._stateItems + .filter((stateItem) => + this._entityReg + .find((reg) => reg.entity_id === stateItem.entity_id) + ?.labels.some((lbl) => filter.value!.includes(lbl)) + ) + .forEach((stateItem) => labelItems.add(stateItem.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._filteredStateItems = items ? [...items] : undefined; + } + + private _clearFilter() { + this._filters = {}; + this._applyFilters(); + } + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); if (this.route.path === "/add") { @@ -418,9 +563,23 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { } } - private _createHelpler() { + private _createHelper() { showHelperDetailDialog(this, {}); } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + hass-tabs-subpage-data-table { + --data-table-row-height: 60px; + } + hass-tabs-subpage-data-table.narrow { + --data-table-row-height: 72px; + } + `, + ]; + } } declare global { From e8dc61ec3697366cb38f446a4296453305f9ad85 Mon Sep 17 00:00:00 2001 From: Yosi Levy <37745463+yosilevy@users.noreply.github.com> Date: Sat, 30 Mar 2024 15:32:29 +0300 Subject: [PATCH 02/14] RTL fixes to new data table (#20283) RTL fixes to new features --- src/components/chips/ha-assist-chip.ts | 3 +++ src/components/ha-color-picker.ts | 9 +++++---- src/components/ha-outlined-text-field.ts | 3 +++ src/layouts/hass-tabs-subpage-data-table.ts | 4 ++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/chips/ha-assist-chip.ts b/src/components/chips/ha-assist-chip.ts index 6e9e6bc7c91b..7071663cd2a7 100644 --- a/src/components/chips/ha-assist-chip.ts +++ b/src/components/chips/ha-assist-chip.ts @@ -56,6 +56,9 @@ export class HaAssistChip extends MdAssistChip { background: var(--ha-assist-chip-active-container-color); opacity: var(--ha-assist-chip-active-container-opacity); } + .label { + font-family: Roboto, sans-serif; + } `, ]; diff --git a/src/components/ha-color-picker.ts b/src/components/ha-color-picker.ts index f5af1d76a5a7..2acd53aa140b 100644 --- a/src/components/ha-color-picker.ts +++ b/src/components/ha-color-picker.ts @@ -6,6 +6,7 @@ import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; import { fireEvent } from "../common/dom/fire_event"; import { stopPropagation } from "../common/dom/stop_propagation"; import "./ha-select"; +import "./ha-list-item"; import { HomeAssistant } from "../types"; import { LocalizeKeys } from "../common/translations/localize"; @@ -53,18 +54,18 @@ export class HaColorPicker extends LitElement { ` : nothing} ${this.defaultColor - ? html` + ? html` ${this.hass.localize(`ui.components.color-picker.default_color`)} - ` + ` : nothing} ${Array.from(THEME_COLORS).map( (color) => html` - + ${this.hass.localize( `ui.components.color-picker.colors.${color}` as LocalizeKeys ) || color} ${this.renderColorCircle(color)} - + ` )} diff --git a/src/components/ha-outlined-text-field.ts b/src/components/ha-outlined-text-field.ts index 0580f65fcbeb..3118c8504923 100644 --- a/src/components/ha-outlined-text-field.ts +++ b/src/components/ha-outlined-text-field.ts @@ -27,6 +27,9 @@ export class HaOutlinedTextField extends MdOutlinedTextField { --md-outlined-field-focus-outline-width: 1px; --mdc-icon-size: var(--md-input-chip-icon-size, 18px); } + .input { + font-family: Roboto, sans-serif; + } `, ]; } diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index b4d4ddc58f26..8db5643ebe59 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -637,6 +637,8 @@ export class HaTabsSubpageDataTable extends LitElement { position: absolute; top: -4px; right: -4px; + inset-inline-end: -4px; + inset-inline-start: initial; min-width: 16px; box-sizing: border-box; border-radius: 50%; @@ -682,6 +684,8 @@ export class HaTabsSubpageDataTable extends LitElement { .selection-bar p { margin-left: 16px; + margin-inline-start: 16px; + margin-inline-end: initial; } ha-assist-chip { From f13dcb4139407350e8089ec776a513b91619c879 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 30 Mar 2024 14:40:13 +0100 Subject: [PATCH 03/14] Fix flickering toast (#20287) --- src/managers/notification-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/notification-manager.ts b/src/managers/notification-manager.ts index a4636d2f7c9c..f7d4a16f8568 100644 --- a/src/managers/notification-manager.ts +++ b/src/managers/notification-manager.ts @@ -27,7 +27,7 @@ class NotificationManager extends LitElement { @query("ha-toast") private _toast!: HaToast | undefined; public async showDialog(parameters: ShowToastParams) { - if (this._parameters) { + if (this._parameters && this._parameters.message !== parameters.message) { this._parameters = undefined; await this.updateComplete; } From f3ba6e799620995809613265906f28072ee52cbe Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 30 Mar 2024 14:44:59 +0100 Subject: [PATCH 04/14] Fix uncaught keyFunction errors when data table filtering (#20285) * Undefined keys * Apply suggestion Co-authored-by: Bram Kragten * Prettier --------- Co-authored-by: Bram Kragten --- src/components/data-table/ha-data-table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index cf442f525f9b..fbd6d9728126 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -386,7 +386,7 @@ export class HaDataTable extends LitElement { `; } - private _keyFunction = (row: DataTableRowData) => row[this.id] || row; + private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row; private _renderRow = (row: DataTableRowData, index: number) => { // not sure how this happens... From 503a7979d0650d25f633b00c0350dd6176381148 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 30 Mar 2024 15:32:34 +0100 Subject: [PATCH 05/14] Fix clearing of filters (#20288) * Fix clearing of filters * Update ha-filter-integrations.ts * Update ha-filter-integrations.ts --- src/components/ha-filter-blueprints.ts | 2 +- src/components/ha-filter-devices.ts | 7 +++- src/components/ha-filter-entities.ts | 11 +++++- src/components/ha-filter-integrations.ts | 42 +++++++++++--------- src/components/ha-filter-labels.ts | 49 +++++++++++++----------- 5 files changed, 66 insertions(+), 45 deletions(-) diff --git a/src/components/ha-filter-blueprints.ts b/src/components/ha-filter-blueprints.ts index f7ef069c5ec4..c20dc197bfc8 100644 --- a/src/components/ha-filter-blueprints.ts +++ b/src/components/ha-filter-blueprints.ts @@ -50,7 +50,7 @@ export class HaFilterBlueprints extends LitElement { ? nothing : html` ${blueprint.metadata.name || id} ` diff --git a/src/components/ha-filter-devices.ts b/src/components/ha-filter-devices.ts index 944b26c87cf1..4b9f2bebc443 100644 --- a/src/components/ha-filter-devices.ts +++ b/src/components/ha-filter-devices.ts @@ -57,7 +57,8 @@ export class HaFilterDevices extends LitElement { ${this._shouldRender ? html` @@ -68,6 +69,8 @@ export class HaFilterDevices extends LitElement { `; } + private _keyFunction = (device) => device?.id; + private _renderItem = (device) => html` { + private _devices = memoizeOne((devices: HomeAssistant["devices"], _value) => { const values = Object.values(devices); return values.sort((a, b) => stringCompare( diff --git a/src/components/ha-filter-entities.ts b/src/components/ha-filter-entities.ts index 5d43de5f1c06..2cffd9945612 100644 --- a/src/components/ha-filter-entities.ts +++ b/src/components/ha-filter-entities.ts @@ -59,7 +59,12 @@ export class HaFilterEntities extends LitElement { ? html` @@ -81,6 +86,8 @@ export class HaFilterEntities extends LitElement { } } + private _keyFunction = (entity) => entity?.entity_id; + private _renderItem = (entity) => html` { + (states: HomeAssistant["states"], type: this["type"], _value) => { const values = Object.values(states); return values .filter( diff --git a/src/components/ha-filter-integrations.ts b/src/components/ha-filter-integrations.ts index 6fd909168c64..5f8b1224b5cd 100644 --- a/src/components/ha-filter-integrations.ts +++ b/src/components/ha-filter-integrations.ts @@ -1,15 +1,16 @@ import { SelectedDetail } from "@material/mwc-list"; 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 { stringCompare } from "../common/string/compare"; -import { haStyleScrollbar } from "../resources/styles"; -import type { HomeAssistant } from "../types"; import { fetchIntegrationManifests, IntegrationManifest, } from "../data/integration"; +import { haStyleScrollbar } from "../resources/styles"; +import type { HomeAssistant } from "../types"; import "./ha-domain-icon"; @customElement("ha-filter-integrations") @@ -47,11 +48,15 @@ export class HaFilterIntegrations extends LitElement { multi class="ha-scrollbar" > - ${this._integrations(this._manifests).map( + ${repeat( + this._integrations(this._manifests, this.value), + (i) => i.domain, (integration) => html` - 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 _integrations = memoizeOne( + (manifest: IntegrationManifest[], _value) => + 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>> ) { - const integrations = this._integrations(this._manifests!); + const integrations = this._integrations(this._manifests!, this.value); if (!ev.detail.index.size) { fireEvent(this, "data-table-filter-changed", { diff --git a/src/components/ha-filter-labels.ts b/src/components/ha-filter-labels.ts index dd7ceada0b93..43c3c10098be 100644 --- a/src/components/ha-filter-labels.ts +++ b/src/components/ha-filter-labels.ts @@ -1,9 +1,10 @@ import { SelectedDetail } from "@material/mwc-list"; import "@material/mwc-menu/mwc-menu-surface"; +import { 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 { mdiPlus } from "@mdi/js"; +import { repeat } from "lit/directives/repeat"; import { computeCssColor } from "../common/color/compute-color"; import { fireEvent } from "../common/dom/fire_event"; import { @@ -12,13 +13,13 @@ import { subscribeLabelRegistry, } from "../data/label_registry"; import { SubscribeMixin } from "../mixins/subscribe-mixin"; +import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant } from "../types"; import "./ha-check-list-item"; import "./ha-expansion-panel"; import "./ha-icon"; import "./ha-label"; -import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail"; @customElement("ha-filter-labels") export class HaFilterLabels extends SubscribeMixin(LitElement) { @@ -63,26 +64,30 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { class="ha-scrollbar" multi > - ${this._labels.map((label) => { - const color = label.color - ? computeCssColor(label.color) - : undefined; - return html` - - ${label.icon - ? html`` - : nothing} - ${label.name} - - `; - })} + ${repeat( + this._labels, + (label) => label.label_id, + (label) => { + const color = label.color + ? computeCssColor(label.color) + : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + } + )} ` : nothing} From 7e3e2247465ef08e510e6a8ce76a3f600a7a6e26 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 30 Mar 2024 21:11:35 +0100 Subject: [PATCH 06/14] Some data table fixes (#20286) --- src/layouts/hass-subpage.ts | 5 ++++- src/layouts/hass-tabs-subpage-data-table.ts | 22 ++++++++++++++++--- src/layouts/hass-tabs-subpage.ts | 4 ++++ .../config/areas/ha-config-areas-dashboard.ts | 1 - .../config/automation/ha-automation-picker.ts | 15 +++++++++++++ .../config/labels/dialog-label-detail.ts | 8 +++++++ src/translations/en.json | 5 +++++ 7 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts index f297a77b189d..cabbeaf689ca 100644 --- a/src/layouts/hass-subpage.ts +++ b/src/layouts/hass-subpage.ts @@ -142,9 +142,12 @@ class HassSubpage extends LitElement { right: calc(16px + env(safe-area-inset-right)); inset-inline-end: calc(16px + env(safe-area-inset-right)); inset-inline-start: initial; - bottom: calc(16px + env(safe-area-inset-bottom)); z-index: 1; + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; } :host([narrow]) #fab.tabs { bottom: calc(84px + env(safe-area-inset-bottom)); diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index 8db5643ebe59..d7d87de93ac3 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -8,7 +8,7 @@ import { mdiArrowDown, mdiArrowUp, mdiClose, - mdiFilterRemove, + mdiFilterVariantRemove, mdiFilterVariant, mdiFormatListChecks, mdiMenuDown, @@ -227,6 +227,9 @@ export class HaTabsSubpageDataTable extends LitElement { class="has-dropdown select-mode-chip" .active=${this._selectMode} @click=${this._enableSelectMode} + .label=${localize( + "ui.components.subpage-data-table.enter_selection_mode" + )} > ` @@ -294,6 +297,9 @@ export class HaTabsSubpageDataTable extends LitElement {

${localize("ui.components.subpage-data-table.selected", { @@ -318,6 +324,9 @@ export class HaTabsSubpageDataTable extends LitElement { slot="navigationIcon" .path=${mdiClose} @click=${this._toggleFilters} + .label=${localize( + "ui.components.subpage-data-table.close_filter" + )} > ${localize( @@ -326,7 +335,11 @@ export class HaTabsSubpageDataTable extends LitElement { >

@@ -347,8 +360,11 @@ export class HaTabsSubpageDataTable extends LitElement { >
diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index c557856eb8a5..ee34ad75e2d3 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -344,6 +344,10 @@ class HassTabsSubpage extends LitElement { inset-inline-start: initial; bottom: calc(16px + env(safe-area-inset-bottom)); z-index: 1; + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; } :host([narrow]) #fab.tabs { bottom: calc(84px + env(safe-area-inset-bottom)); diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index e322274d341c..f4a38567d1b2 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -424,7 +424,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { } .floor { --primary-color: var(--secondary-text-color); - margin-inline-end: 8px; } .warning { color: var(--error-color); diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 5057c1e4d1eb..d99731e9547d 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -1,6 +1,7 @@ import { consume } from "@lit-labs/context"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { + mdiCog, mdiContentDuplicate, mdiDelete, mdiHelpCircle, @@ -287,6 +288,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { ), action: () => this._showInfo(automation), }, + { + path: mdiCog, + label: this.hass.localize( + "ui.panel.config.automation.picker.show_settings" + ), + action: () => this._showSettings(automation), + }, { path: mdiTag, label: this.hass.localize( @@ -637,6 +645,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { fireEvent(this, "hass-more-info", { entityId: automation.entity_id }); } + private _showSettings(automation: any) { + fireEvent(this, "hass-more-info", { + entityId: automation.entity_id, + view: "settings", + }); + } + private _runActions(automation: any) { triggerAutomationActions(this.hass, automation.entity_id); } diff --git a/src/panels/config/labels/dialog-label-detail.ts b/src/panels/config/labels/dialog-label-detail.ts index c5e36cb72356..b0e06386272b 100644 --- a/src/panels/config/labels/dialog-label-detail.ts +++ b/src/panels/config/labels/dialog-label-detail.ts @@ -49,11 +49,19 @@ class DialogLabelDetail this._icon = ""; this._color = ""; } + document.body.addEventListener("keydown", this._handleKeyPress); } + private _handleKeyPress = (ev: KeyboardEvent) => { + if (ev.key === "Escape") { + ev.stopPropagation(); + } + }; + public closeDialog(): void { this._params = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); + document.body.removeEventListener("keydown", this._handleKeyPress); } protected render() { diff --git a/src/translations/en.json b/src/translations/en.json index 8e282fba9de2..c4877539c13a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -501,6 +501,10 @@ }, "subpage-data-table": { "filters": "Filters", + "clear_filter": "Clear filter", + "close_filter": "Close filters", + "exit_selection_mode": "Exit selection mode", + "enter_selection_mode": "Enter selection mode", "sort_by": "Sort by {sortColumn}", "group_by": "Group by {groupColumn}", "dont_group_by": "Don't group", @@ -2669,6 +2673,7 @@ "edit_automation": "Edit automation", "dev_automation": "Debug automation", "show_info_automation": "Show info about automation", + "show_settings": "Show settings", "delete": "[%key:ui::common::delete%]", "delete_confirm_title": "Delete automation?", "delete_confirm_text": "{name} will be permanently deleted.", From b202a36feb9d44a49b4189864d1dc3d0f787fe79 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sun, 31 Mar 2024 12:28:28 +0200 Subject: [PATCH 07/14] Clean menu implementation (#20294) * Clean menu * Clean up imports * Fix imports --- src/components/ha-menu-item.ts | 37 +++++++++++++++++ src/components/ha-menu.ts | 22 ++++++++++ src/layouts/hass-tabs-subpage-data-table.ts | 45 +++++++-------------- 3 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 src/components/ha-menu-item.ts create mode 100644 src/components/ha-menu.ts diff --git a/src/components/ha-menu-item.ts b/src/components/ha-menu-item.ts new file mode 100644 index 000000000000..e96e867f6f83 --- /dev/null +++ b/src/components/ha-menu-item.ts @@ -0,0 +1,37 @@ +import { customElement } from "lit/decorators"; +import "element-internals-polyfill"; +import { CSSResult, css } from "lit"; +import { MdMenuItem } from "@material/web/menu/menu-item"; + +@customElement("ha-menu-item") +export class HaMenuItem extends MdMenuItem { + static override styles: CSSResult[] = [ + ...MdMenuItem.styles, + css` + :host { + --ha-icon-display: block; + --md-sys-color-primary: var(--primary-text-color); + --md-sys-color-on-primary: var(--primary-text-color); + --md-sys-color-secondary: var(--secondary-text-color); + --md-sys-color-surface: var(--card-background-color); + --md-sys-color-on-surface: var(--primary-text-color); + --md-sys-color-on-surface-variant: var(--secondary-text-color); + --md-sys-color-secondary-container: rgba( + var(--rgb-primary-color), + 0.15 + ); + --md-sys-color-on-secondary-container: var(--text-primary-color); + --mdc-icon-size: 16px; + + --md-sys-color-on-primary-container: var(--primary-text-color); + --md-sys-color-on-secondary-container: var(--primary-text-color); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-menu-item": HaMenuItem; + } +} diff --git a/src/components/ha-menu.ts b/src/components/ha-menu.ts new file mode 100644 index 000000000000..d1a414698427 --- /dev/null +++ b/src/components/ha-menu.ts @@ -0,0 +1,22 @@ +import { customElement } from "lit/decorators"; +import "element-internals-polyfill"; +import { CSSResult, css } from "lit"; +import { MdMenu } from "@material/web/menu/menu"; + +@customElement("ha-menu") +export class HaMenu extends MdMenu { + static override styles: CSSResult[] = [ + ...MdMenu.styles, + css` + :host { + --md-sys-color-surface-container: var(--card-background-color); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-menu": HaMenu; + } +} diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index d7d87de93ac3..b58d0731652d 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -1,9 +1,6 @@ import { ResizeController } from "@lit-labs/observers/resize-controller"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import "@material/mwc-button/mwc-button"; -import "@material/web/menu/menu"; -import type { MdMenu } from "@material/web/menu/menu"; -import "@material/web/menu/menu-item"; import { mdiArrowDown, mdiArrowUp, @@ -36,9 +33,12 @@ import type { } from "../components/data-table/ha-data-table"; import "../components/ha-dialog"; import "../components/search-input-outlined"; +import "../components/ha-menu"; +import "../components/ha-menu-item"; import type { HomeAssistant, Route } from "../types"; import "./hass-tabs-subpage"; import type { PageNavigation } from "./hass-tabs-subpage"; +import type { HaMenu } from "../components/ha-menu"; declare global { // for fire event @@ -177,9 +177,9 @@ export class HaTabsSubpageDataTable extends LitElement { @query("ha-data-table", true) private _dataTable!: HaDataTable; - @query("#group-by-menu") private _groupByMenu!: MdMenu; + @query("#group-by-menu") private _groupByMenu!: HaMenu; - @query("#sort-by-menu") private _sortByMenu!: MdMenu; + @query("#sort-by-menu") private _sortByMenu!: HaMenu; private _showPaneController = new ResizeController(this, { callback: (entries) => entries[0]?.contentRect.width > 750, @@ -425,37 +425,37 @@ export class HaTabsSubpageDataTable extends LitElement { `}
- + ${Object.entries(this.columns).map(([id, column]) => column.groupable ? html` - ${column.title || column.label} - + ` : nothing )}
  • - ${localize( "ui.components.subpage-data-table.dont_group_by" - )} -
    - + + ${Object.entries(this.columns).map(([id, column]) => column.sortable ? html` - + ` : nothing )} - + `; } @@ -732,21 +732,6 @@ export class HaTabsSubpageDataTable extends LitElement { display: flex; flex-direction: column; } - /* TODO: Migrate to ha-menu and ha-menu-item */ - md-menu { - --md-menu-container-color: var(--card-background-color); - } - md-menu-item { - --md-menu-item-label-text-color: var(--primary-text-color); - --mdc-icon-size: 16px; - --md-menu-item-selected-container-color: rgba( - var(--rgb-primary-color), - 0.15 - ); - } - md-menu-item.selected { - --md-menu-item-label-text-color: var(--primary-color); - } #sort-by-anchor, #group-by-anchor { --md-assist-chip-trailing-space: 8px; From 4f8415e8a78b919e89b389e3fbd3a223c6b87de3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:13:46 +0200 Subject: [PATCH 08/14] Update dependency @codemirror/view to v6.26.1 (#20300) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cece79f41b8f..95e64ad94fcb 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@codemirror/legacy-modes": "6.3.3", "@codemirror/search": "6.5.6", "@codemirror/state": "6.4.1", - "@codemirror/view": "6.26.0", + "@codemirror/view": "6.26.1", "@egjs/hammerjs": "2.0.17", "@formatjs/intl-datetimeformat": "6.12.3", "@formatjs/intl-displaynames": "6.6.6", diff --git a/yarn.lock b/yarn.lock index 93b495eaf293..f1095039280a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1526,14 +1526,14 @@ __metadata: languageName: node linkType: hard -"@codemirror/view@npm:6.26.0, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0": - version: 6.26.0 - resolution: "@codemirror/view@npm:6.26.0" +"@codemirror/view@npm:6.26.1, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0": + version: 6.26.1 + resolution: "@codemirror/view@npm:6.26.1" dependencies: "@codemirror/state": "npm:^6.4.0" style-mod: "npm:^4.1.0" w3c-keyname: "npm:^2.2.4" - checksum: 10/d4ef249044cbc293a7267c83e08671a68646fd7bbe1efb8d205c01385f157c93918eabeaedb62a4cc10598ab63818ac749cec4f6355fe0404d9d4beb7857c31f + checksum: 10/6d2b19b2439c36b2712d3560eeb0c198ad2ee442ad22641c2b4bce94077812cffbb52ca12328219d3b9663b2dd0ffc63481432a2550839e5c7a7a53704e82a9a languageName: node linkType: hard @@ -9604,7 +9604,7 @@ __metadata: "@codemirror/legacy-modes": "npm:6.3.3" "@codemirror/search": "npm:6.5.6" "@codemirror/state": "npm:6.4.1" - "@codemirror/view": "npm:6.26.0" + "@codemirror/view": "npm:6.26.1" "@egjs/hammerjs": "npm:2.0.17" "@formatjs/intl-datetimeformat": "npm:6.12.3" "@formatjs/intl-displaynames": "npm:6.6.6" From 1ce3347c2e91403823d34b994ea3b876e8dddc8c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 10:14:17 +0200 Subject: [PATCH 09/14] Add multi select to automations (#20291) * Add multi select to automations * allow to clear category, add icons * use popover * revert changes to group. by and sort menu, fix dark mode * ha-menu * responsive --- src/components/chips/ha-assist-chip.ts | 12 +- src/components/data-table/ha-data-table.ts | 12 +- src/components/ha-button-menu-new.ts | 89 ++++++ src/components/ha-sub-menu.ts | 38 +++ src/layouts/hass-tabs-subpage-data-table.ts | 93 ++++-- .../config/automation/ha-automation-picker.ts | 294 ++++++++++++++++-- .../config/entities/ha-config-entities.ts | 4 +- src/resources/styles-data.ts | 5 +- src/translations/en.json | 13 +- 9 files changed, 492 insertions(+), 68 deletions(-) create mode 100644 src/components/ha-button-menu-new.ts create mode 100644 src/components/ha-sub-menu.ts diff --git a/src/components/chips/ha-assist-chip.ts b/src/components/chips/ha-assist-chip.ts index 7071663cd2a7..5a7b0d1fd528 100644 --- a/src/components/chips/ha-assist-chip.ts +++ b/src/components/chips/ha-assist-chip.ts @@ -22,14 +22,6 @@ export class HaAssistChip extends MdAssistChip { ); --md-assist-chip-outline-color: var(--outline-color); --md-assist-chip-label-text-weight: 400; - --ha-assist-chip-filled-container-color: rgba( - var(--rgb-primary-text-color), - 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 **/ .filled { @@ -52,6 +44,10 @@ export class HaAssistChip extends MdAssistChip { margin-inline-end: unset; margin-inline-start: var(--_icon-label-space); } + ::before { + background: var(--ha-assist-chip-container-color); + opacity: var(--ha-assist-chip-container-opacity); + } :where(.active)::before { background: var(--ha-assist-chip-active-container-color); opacity: var(--ha-assist-chip-active-container-opacity); diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index fbd6d9728126..e514526c3bd1 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -181,6 +181,13 @@ export class HaDataTable extends LitElement { this._checkedRowsChanged(); } + public selectAll(): void { + this._checkedRows = this._filteredData + .filter((data) => data.selectable !== false) + .map((data) => data[this.id]); + this._checkedRowsChanged(); + } + public connectedCallback() { super.connectedCallback(); if (this._items.length) { @@ -593,10 +600,7 @@ export class HaDataTable extends LitElement { private _handleHeaderRowCheckboxClick(ev: Event) { const checkbox = ev.target as HaCheckbox; if (checkbox.checked) { - this._checkedRows = this._filteredData - .filter((data) => data.selectable !== false) - .map((data) => data[this.id]); - this._checkedRowsChanged(); + this.selectAll(); } else { this._checkedRows = []; this._checkedRowsChanged(); diff --git a/src/components/ha-button-menu-new.ts b/src/components/ha-button-menu-new.ts new file mode 100644 index 000000000000..3ec12b11081a --- /dev/null +++ b/src/components/ha-button-menu-new.ts @@ -0,0 +1,89 @@ +import { Button } from "@material/mwc-button"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, query } from "lit/decorators"; +import { FOCUS_TARGET } from "../dialogs/make-dialog-manager"; +import type { HaIconButton } from "./ha-icon-button"; +import "./ha-menu"; +import type { HaMenu } from "./ha-menu"; + +@customElement("ha-button-menu-new") +export class HaButtonMenuNew extends LitElement { + protected readonly [FOCUS_TARGET]; + + @property({ type: Boolean }) public disabled = false; + + @property() public positioning?: "fixed" | "absolute" | "popover"; + + @property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow = + false; + + @query("ha-menu", true) private _menu!: HaMenu; + + public get items() { + return this._menu.items; + } + + public override focus() { + if (this._menu.open) { + this._menu.focus(); + } else { + this._triggerButton?.focus(); + } + } + + protected render(): TemplateResult { + return html` +
    + +
    + + + + `; + } + + private _handleClick(): void { + if (this.disabled) { + return; + } + this._menu.anchorElement = this; + if (this._menu.open) { + this._menu.close(); + } else { + this._menu.show(); + } + } + + private get _triggerButton() { + return this.querySelector( + 'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"], ha-assist-chip[slot="trigger"]' + ) as HaIconButton | Button | null; + } + + private _setTriggerAria() { + if (this._triggerButton) { + this._triggerButton.ariaHasPopup = "menu"; + } + } + + static get styles(): CSSResultGroup { + return css` + :host { + display: inline-block; + position: relative; + } + ::slotted([disabled]) { + color: var(--disabled-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-button-menu-new": HaButtonMenuNew; + } +} diff --git a/src/components/ha-sub-menu.ts b/src/components/ha-sub-menu.ts new file mode 100644 index 000000000000..15a5afdc47d7 --- /dev/null +++ b/src/components/ha-sub-menu.ts @@ -0,0 +1,38 @@ +import { customElement } from "lit/decorators"; +import "element-internals-polyfill"; +import { CSSResult, css } from "lit"; +import { MdSubMenu } from "@material/web/menu/sub-menu"; + +@customElement("ha-sub-menu") +// @ts-expect-error +export class HaSubMenu extends MdSubMenu { + static override styles: CSSResult[] = [ + MdSubMenu.styles, + css` + :host { + --ha-icon-display: block; + --md-sys-color-primary: var(--primary-text-color); + --md-sys-color-on-primary: var(--primary-text-color); + --md-sys-color-secondary: var(--secondary-text-color); + --md-sys-color-surface: var(--card-background-color); + --md-sys-color-on-surface: var(--primary-text-color); + --md-sys-color-on-surface-variant: var(--secondary-text-color); + --md-sys-color-secondary-container: rgba( + var(--rgb-primary-color), + 0.15 + ); + --md-sys-color-on-secondary-container: var(--text-primary-color); + --mdc-icon-size: 16px; + + --md-sys-color-on-primary-container: var(--primary-text-color); + --md-sys-color-on-secondary-container: var(--primary-text-color); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-sub-menu": HaSubMenu; + } +} diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index b58d0731652d..53a60e37e3db 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -1,12 +1,13 @@ import { ResizeController } from "@lit-labs/observers/resize-controller"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import "@material/mwc-button/mwc-button"; +import "@material/web/divider/divider"; import { mdiArrowDown, mdiArrowUp, mdiClose, - mdiFilterVariantRemove, mdiFilterVariant, + mdiFilterVariantRemove, mdiFormatListChecks, mdiMenuDown, } from "@mdi/js"; @@ -31,14 +32,14 @@ import type { HaDataTable, SortingDirection, } from "../components/data-table/ha-data-table"; +import "../components/ha-button-menu-new"; import "../components/ha-dialog"; -import "../components/search-input-outlined"; -import "../components/ha-menu"; +import { HaMenu } from "../components/ha-menu"; import "../components/ha-menu-item"; +import "../components/search-input-outlined"; import type { HomeAssistant, Route } from "../types"; import "./hass-tabs-subpage"; import type { PageNavigation } from "./hass-tabs-subpage"; -import type { HaMenu } from "../components/ha-menu"; declare global { // for fire event @@ -227,7 +228,7 @@ export class HaTabsSubpageDataTable extends LitElement { class="has-dropdown select-mode-chip" .active=${this._selectMode} @click=${this._enableSelectMode} - .label=${localize( + .title=${localize( "ui.components.subpage-data-table.enter_selection_mode" )} > @@ -255,8 +256,11 @@ export class HaTabsSubpageDataTable extends LitElement { id="sort-by-anchor" @click=${this._toggleSortBy} > - + + ` : nothing; @@ -293,7 +297,7 @@ export class HaTabsSubpageDataTable extends LitElement { > ${this._selectMode ? html`
    -
    +
    + + + + + ${localize("ui.components.subpage-data-table.select_all")} + + ${localize("ui.components.subpage-data-table.select_none")} + + + ${localize( + "ui.components.subpage-data-table.close_select_mode" + )} + +

    ${localize("ui.components.subpage-data-table.selected", { selected: this.selected || "0", @@ -440,16 +475,15 @@ export class HaTabsSubpageDataTable extends LitElement { ` : nothing )} -

  • + ${localize( - "ui.components.subpage-data-table.dont_group_by" - )} + ${localize("ui.components.subpage-data-table.dont_group_by")} + ${Object.entries(this.columns).map(([id, column]) => @@ -458,6 +492,7 @@ export class HaTabsSubpageDataTable extends LitElement { @@ -494,8 +529,6 @@ export class HaTabsSubpageDataTable extends LitElement { } private _handleSortBy(ev) { - ev.stopPropagation(); - ev.preventDefault(); const columnId = ev.currentTarget.value; if (!this._sortDirection || this._sortColumn !== columnId) { this._sortDirection = "asc"; @@ -520,6 +553,14 @@ export class HaTabsSubpageDataTable extends LitElement { this._dataTable.clearSelection(); } + private _selectAll() { + this._dataTable.selectAll(); + } + + private _selectNone() { + this._dataTable.clearSelection(); + } + private _handleSearchChange(ev: CustomEvent) { if (this.filter === ev.detail.value) { return; @@ -687,23 +728,31 @@ export class HaTabsSubpageDataTable extends LitElement { padding: 8px 12px; box-sizing: border-box; font-size: 14px; + --ha-assist-chip-container-color: var(--primary-background-color); + } + + .selection-controls { + display: flex; + align-items: center; + gap: 8px; + } + + .selection-controls p { + margin-left: 8px; + margin-inline-start: 8px; + margin-inline-end: initial; } .center-vertical { display: flex; align-items: center; + gap: 8px; } .relative { position: relative; } - .selection-bar p { - margin-left: 16px; - margin-inline-start: 16px; - margin-inline-end: initial; - } - ha-assist-chip { --ha-assist-chip-container-shape: 10px; } @@ -732,8 +781,10 @@ export class HaTabsSubpageDataTable extends LitElement { display: flex; flex-direction: column; } + #sort-by-anchor, - #group-by-anchor { + #group-by-anchor, + ha-button-menu-new ha-assist-chip { --md-assist-chip-trailing-space: 8px; } `; diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index d99731e9547d..c53a786621a3 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -1,17 +1,22 @@ import { consume } from "@lit-labs/context"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { + mdiChevronRight, mdiCog, mdiContentDuplicate, mdiDelete, + mdiDotsVertical, mdiHelpCircle, mdiInformationOutline, + mdiMenuDown, mdiPlay, mdiPlayCircleOutline, mdiPlus, mdiRobotHappy, mdiStopCircleOutline, mdiTag, + mdiToggleSwitch, + mdiToggleSwitchOffOutline, mdiTransitConnection, } from "@mdi/js"; import { differenceInDays } from "date-fns/esm"; @@ -28,6 +33,7 @@ import { import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; +import { computeCssColor } from "../../../common/color/compute-color"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { relativeTime } from "../../../common/datetime/relative_time"; @@ -39,6 +45,7 @@ import "../../../components/chips/ha-assist-chip"; import type { DataTableColumnContainer, RowClickedEvent, + SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; import "../../../components/entity/ha-entity-toggle"; @@ -51,6 +58,8 @@ import "../../../components/ha-filter-floor-areas"; import "../../../components/ha-filter-labels"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-overflow-menu"; +import "../../../components/ha-menu-item"; +import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; import { AutomationEntity, @@ -67,7 +76,11 @@ import { } from "../../../data/category_registry"; import { fullEntitiesContext } from "../../../data/context"; import { UNAVAILABLE } from "../../../data/entity"; -import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { + EntityRegistryEntry, + UpdateEntityRegistryEntryResult, + updateEntityRegistryEntry, +} from "../../../data/entity_registry"; import { LabelRegistryEntry, subscribeLabelRegistry, @@ -80,8 +93,9 @@ import { import "../../../layouts/hass-tabs-subpage-data-table"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; -import { HomeAssistant, Route } from "../../../types"; +import { HomeAssistant, Route, ServiceCallResponse } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; +import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { configSections } from "../ha-panel-config"; import { showNewAutomationDialog } from "./show-dialog-new-automation"; @@ -117,6 +131,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { @state() private _expandedFilter?: string; + @state() private _selected: string[] = []; + @state() _categories!: CategoryRegistryEntry[]; @@ -374,6 +390,40 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { } protected render(): TemplateResult { + 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" + )} +
    +
    `; + const labelItems = html` ${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })}`; + return html` filter.value?.length - ).length} + .filters=${ + Object.values(this._filters).filter((filter) => filter.value?.length) + .length + } .columns=${this._columns( this.narrow, this.hass.localize, @@ -474,36 +528,156 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { .narrow=${this.narrow} @expanded-changed=${this._filterExpanded} > - ${!this.automations.length - ? html`
    - -

    - ${this.hass.localize( - "ui.panel.config.automation.picker.empty_header" - )} -

    -

    + ${ + !this.narrow + ? html` + + + + ${categoryItems} + + ${this.hass.dockedSidebar === "docked" + ? nothing + : html` + + + + ${labelItems} + `}` + : nothing + } + + ${ + 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 + } + + +
    ${this.hass.localize( - "ui.panel.config.automation.picker.empty_text_1" + "ui.panel.config.automation.picker.bulk_actions.enable" )} -

    -

    +

    +
    + + +
    ${this.hass.localize( - "ui.panel.config.automation.picker.empty_text_2", - { user: this.hass.user?.name || "Alice" } + "ui.panel.config.automation.picker.bulk_actions.disable" )} -

    - - - ${this.hass.localize("ui.panel.config.common.learn_more")} - - -
    ` - : nothing} +
    +
    + + ${ + !this.automations.length + ? html`
    + +

    + ${this.hass.localize( + "ui.panel.config.automation.picker.empty_header" + )} +

    +

    + ${this.hass.localize( + "ui.panel.config.automation.picker.empty_text_1" + )} +

    +

    + ${this.hass.localize( + "ui.panel.config.automation.picker.empty_text_2", + { user: this.hass.user?.name || "Alice" } + )} +

    + + + ${this.hass.localize("ui.panel.config.common.learn_more")} + + +
    ` + : nothing + } + ): void { + this._selected = ev.detail.value; + } + private _createNew() { if (isComponentLoaded(this.hass, "blueprint")) { showNewAutomationDialog(this, { mode: "automation" }); @@ -799,6 +979,48 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { } } + private async _handleBulkCategory(ev) { + const category = ev.currentTarget.value; + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + categories: { automation: category }, + }) + ); + }); + await Promise.all(promises); + } + + private async _handleBulkLabel(ev) { + const label = ev.currentTarget.value; + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + labels: this.hass.entities[entityId].labels.concat(label), + }) + ); + }); + await Promise.all(promises); + } + + private async _handleBulkEnable() { + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push(turnOnOffEntity(this.hass, entityId, true)); + }); + await Promise.all(promises); + } + + private async _handleBulkDisable() { + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push(turnOnOffEntity(this.hass, entityId, false)); + }); + await Promise.all(promises); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -814,6 +1036,16 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { --mdc-icon-size: 80px; max-width: 500px; } + 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/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 7df4b35bd833..c0d92b96b794 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -527,11 +527,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { .filters=${Object.values(this._filters).filter( (filter) => filter.value?.length ).length} - .selected=${this._selectedEntities.length} .filter=${this._filter} selectable - clickable + .selected=${this._selectedEntities.length} @selection-changed=${this._handleSelectionChanged} + clickable @clear-filter=${this._clearFilter} @search-changed=${this._handleSearchChange} @row-click=${this._openEditEntry} diff --git a/src/resources/styles-data.ts b/src/resources/styles-data.ts index 8670024ca6ee..ae04a4c4c473 100644 --- a/src/resources/styles-data.ts +++ b/src/resources/styles-data.ts @@ -143,7 +143,10 @@ export const derivedStyles = { "mdc-select-disabled-ink-color": "var(--input-disabled-ink-color)", "mdc-select-dropdown-icon-color": "var(--input-dropdown-icon-color)", "mdc-select-disabled-dropdown-icon-color": "var(--input-disabled-ink-color)", - + "ha-assist-chip-filled-container-color": + "rgba(var(--rgb-primary-text-color),0.15)", + "ha-assist-chip-active-container-color": + "rgba(var(--rgb-primary-color),0.15)", "chip-background-color": "rgba(var(--rgb-primary-text-color), 0.15)", // Vaadin "material-body-text-color": "var(--primary-text-color)", diff --git a/src/translations/en.json b/src/translations/en.json index c4877539c13a..9a68c368b7b1 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -509,7 +509,10 @@ "group_by": "Group by {groupColumn}", "dont_group_by": "Don't group", "select": "Select", - "selected": "Selected {selected}" + "selected": "Selected {selected}", + "close_select_mode": "Close selection mode", + "select_all": "Select all", + "select_none": "Select none" }, "config-entry-picker": { "config_entry": "Integration" @@ -2694,6 +2697,14 @@ "state": "State", "category": "Category" }, + "bulk_action": "Action", + "bulk_actions": { + "move_category": "Move to category", + "no_category": "No category", + "add_label": "Add label", + "enable": "Enable", + "disable": "Disable" + }, "empty_header": "Start automating", "empty_text_1": "Automations make Home Assistant automatically respond to things happening in and around your home.", "empty_text_2": "Automations connect triggers to actions in a ''when trigger then action'' fashion with optional conditions. For example: ''When the sun sets and if {user} is home, then turn on the lights''." From 85f201637195313e87c5ec1769a6a0ec20bd6c8c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 10:43:50 +0200 Subject: [PATCH 10/14] Add floor selector (#20295) --- demo/src/stubs/area_registry.ts | 9 +- demo/src/stubs/device_registry.ts | 9 +- demo/src/stubs/floor_registry.ts | 7 + demo/src/stubs/label_registry.ts | 7 + gallery/src/pages/components/ha-selector.ts | 61 ++++++- src/components/ha-floor-picker.ts | 2 +- src/components/ha-floors-picker.ts | 169 ++++++++++++++++++ .../ha-selector/ha-selector-area.ts | 16 +- .../ha-selector/ha-selector-floor.ts | 153 ++++++++++++++++ src/components/ha-selector/ha-selector.ts | 1 + src/data/selector.ts | 9 + 11 files changed, 430 insertions(+), 13 deletions(-) create mode 100644 demo/src/stubs/floor_registry.ts create mode 100644 demo/src/stubs/label_registry.ts create mode 100644 src/components/ha-floors-picker.ts create mode 100644 src/components/ha-selector/ha-selector-floor.ts diff --git a/demo/src/stubs/area_registry.ts b/demo/src/stubs/area_registry.ts index b7d8e5a34b1c..59dd77ffe810 100644 --- a/demo/src/stubs/area_registry.ts +++ b/demo/src/stubs/area_registry.ts @@ -4,4 +4,11 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; export const mockAreaRegistry = ( hass: MockHomeAssistant, data: AreaRegistryEntry[] = [] -) => hass.mockWS("config/area_registry/list", () => data); +) => { + hass.mockWS("config/area_registry/list", () => data); + const areas = {}; + data.forEach((area) => { + areas[area.area_id] = area; + }); + hass.updateHass({ areas }); +}; diff --git a/demo/src/stubs/device_registry.ts b/demo/src/stubs/device_registry.ts index 28c47e4a96e1..d1ab8025ee4d 100644 --- a/demo/src/stubs/device_registry.ts +++ b/demo/src/stubs/device_registry.ts @@ -4,4 +4,11 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; export const mockDeviceRegistry = ( hass: MockHomeAssistant, data: DeviceRegistryEntry[] = [] -) => hass.mockWS("config/device_registry/list", () => data); +) => { + hass.mockWS("config/device_registry/list", () => data); + const devices = {}; + data.forEach((device) => { + devices[device.id] = device; + }); + hass.updateHass({ devices }); +}; diff --git a/demo/src/stubs/floor_registry.ts b/demo/src/stubs/floor_registry.ts new file mode 100644 index 000000000000..c962f07a5c16 --- /dev/null +++ b/demo/src/stubs/floor_registry.ts @@ -0,0 +1,7 @@ +import { FloorRegistryEntry } from "../../../src/data/floor_registry"; +import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockFloorRegistry = ( + hass: MockHomeAssistant, + data: FloorRegistryEntry[] = [] +) => hass.mockWS("config/floor_registry/list", () => data); diff --git a/demo/src/stubs/label_registry.ts b/demo/src/stubs/label_registry.ts new file mode 100644 index 000000000000..27ca8fdc8e9a --- /dev/null +++ b/demo/src/stubs/label_registry.ts @@ -0,0 +1,7 @@ +import { LabelRegistryEntry } from "../../../src/data/label_registry"; +import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockLabelRegistry = ( + hass: MockHomeAssistant, + data: LabelRegistryEntry[] = [] +) => hass.mockWS("config/label_registry/list", () => data); diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 15e2aae1adc4..3824d9bb18a5 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -17,6 +17,10 @@ import { provideHass } from "../../../../src/fake_data/provide_hass"; import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; import type { HomeAssistant } from "../../../../src/types"; import "../../components/demo-black-white-row"; +import { FloorRegistryEntry } from "../../../../src/data/floor_registry"; +import { LabelRegistryEntry } from "../../../../src/data/label_registry"; +import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry"; +import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry"; const ENTITIES = [ getEntity("alarm_control_panel", "alarm", "disarmed", { @@ -100,7 +104,7 @@ const DEVICES = [ const AREAS: AreaRegistryEntry[] = [ { area_id: "backyard", - floor_id: null, + floor_id: "ground", name: "Backyard", icon: null, picture: null, @@ -109,7 +113,7 @@ const AREAS: AreaRegistryEntry[] = [ }, { area_id: "bedroom", - floor_id: null, + floor_id: "first", name: "Bedroom", icon: "mdi:bed", picture: null, @@ -118,7 +122,7 @@ const AREAS: AreaRegistryEntry[] = [ }, { area_id: "livingroom", - floor_id: null, + floor_id: "ground", name: "Livingroom", icon: "mdi:sofa", picture: null, @@ -127,6 +131,45 @@ const AREAS: AreaRegistryEntry[] = [ }, ]; +const FLOORS: FloorRegistryEntry[] = [ + { + floor_id: "ground", + name: "Ground floor", + level: 0, + icon: null, + aliases: [], + }, + { + floor_id: "first", + name: "First floor", + level: 1, + icon: "mdi:numeric-1", + aliases: [], + }, + { + floor_id: "second", + name: "Second floor", + level: 2, + icon: "mdi:numeric-2", + aliases: [], + }, +]; + +const LABELS: LabelRegistryEntry[] = [ + { + label_id: "energy", + name: "Energy", + icon: null, + color: "yellow", + }, + { + label_id: "entertainment", + name: "Entertainment", + icon: "mdi:popcorn", + color: "blue", + }, +]; + const SCHEMAS: { name: string; input: Record; @@ -134,7 +177,12 @@ const SCHEMAS: { { name: "One of each", input: { + label: { name: "Label", selector: { label: {} } }, + floor: { name: "Floor", selector: { floor: {} } }, + area: { name: "Area", selector: { area: {} } }, + device: { name: "Device", selector: { device: {} } }, entity: { name: "Entity", selector: { entity: {} } }, + target: { name: "Target", selector: { target: {} } }, state: { name: "State", selector: { state: { entity_id: "alarm_control_panel.alarm" } }, @@ -143,15 +191,12 @@ const SCHEMAS: { name: "Attribute", selector: { attribute: { entity_id: "" } }, }, - device: { name: "Device", selector: { device: {} } }, config_entry: { name: "Integration", selector: { config_entry: {} }, }, duration: { name: "Duration", selector: { duration: {} } }, addon: { name: "Addon", selector: { addon: {} } }, - area: { name: "Area", selector: { area: {} } }, - target: { name: "Target", selector: { target: {} } }, number_box: { name: "Number Box", selector: { @@ -300,6 +345,8 @@ const SCHEMAS: { entity: { name: "Entity", selector: { entity: { multiple: true } } }, device: { name: "Device", selector: { device: { multiple: true } } }, area: { name: "Area", selector: { area: { multiple: true } } }, + floor: { name: "Floor", selector: { floor: { multiple: true } } }, + label: { name: "Label", selector: { label: { multiple: true } } }, select: { name: "Select Multiple", selector: { @@ -356,6 +403,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { mockDeviceRegistry(hass, DEVICES); mockConfigEntries(hass); mockAreaRegistry(hass, AREAS); + mockFloorRegistry(hass, FLOORS); + mockLabelRegistry(hass, LABELS); mockHassioSupervisor(hass); hass.mockWS("auth/sign_path", (params) => params); hass.mockWS("media_player/browse_media", this._browseMedia); diff --git a/src/components/ha-floor-picker.ts b/src/components/ha-floor-picker.ts index 59886ffa292d..9ac37cf746b1 100644 --- a/src/components/ha-floor-picker.ts +++ b/src/components/ha-floor-picker.ts @@ -274,7 +274,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { if (areaIds) { const floorAreaLookup = getFloorAreaLookup(areas); outputFloors = outputFloors.filter((floor) => - floorAreaLookup[floor.floor_id].some((area) => + floorAreaLookup[floor.floor_id]?.some((area) => areaIds!.includes(area.area_id) ) ); diff --git a/src/components/ha-floors-picker.ts b/src/components/ha-floors-picker.ts new file mode 100644 index 000000000000..e5f0e39655fc --- /dev/null +++ b/src/components/ha-floors-picker.ts @@ -0,0 +1,169 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import { SubscribeMixin } from "../mixins/subscribe-mixin"; +import type { HomeAssistant } from "../types"; +import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; +import "./ha-floor-picker"; + +@customElement("ha-floors-picker") +export class HaFloorsPicker extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public label?: string; + + @property({ type: Array }) public value?: string[]; + + @property() public helper?: string; + + @property() public placeholder?: string; + + @property({ type: Boolean, attribute: "no-add" }) + public noAdd = false; + + /** + * Show only floors with entities from specific domains. + * @type {Array} + * @attr include-domains + */ + @property({ type: Array, attribute: "include-domains" }) + public includeDomains?: string[]; + + /** + * Show no floors with entities of these domains. + * @type {Array} + * @attr exclude-domains + */ + @property({ type: Array, attribute: "exclude-domains" }) + public excludeDomains?: string[]; + + /** + * Show only floors with entities of these device classes. + * @type {Array} + * @attr include-device-classes + */ + @property({ type: Array, attribute: "include-device-classes" }) + public includeDeviceClasses?: string[]; + + @property({ attribute: false }) + public deviceFilter?: HaDevicePickerDeviceFilterFunc; + + @property({ attribute: false }) + public entityFilter?: (entity: HassEntity) => boolean; + + @property({ attribute: "picked-floor-label" }) + public pickedFloorLabel?: string; + + @property({ attribute: "pick-floor-label" }) + public pickFloorLabel?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = false; + + protected render() { + if (!this.hass) { + return nothing; + } + + const currentFloors = this._currentFloors; + return html` + ${currentFloors.map( + (floor) => html` +
    + +
    + ` + )} +
    + +
    + `; + } + + private get _currentFloors(): string[] { + return this.value || []; + } + + private async _updateFloors(floors) { + this.value = floors; + + fireEvent(this, "value-changed", { + value: floors, + }); + } + + private _floorChanged(ev: CustomEvent) { + ev.stopPropagation(); + const curValue = (ev.currentTarget as any).curValue; + const newValue = ev.detail.value; + if (newValue === curValue) { + return; + } + const currentFloors = this._currentFloors; + if (!newValue || currentFloors.includes(newValue)) { + this._updateFloors(currentFloors.filter((ent) => ent !== curValue)); + return; + } + this._updateFloors( + currentFloors.map((ent) => (ent === curValue ? newValue : ent)) + ); + } + + private _addFloor(ev: CustomEvent) { + ev.stopPropagation(); + + const toAdd = ev.detail.value; + if (!toAdd) { + return; + } + (ev.currentTarget as any).value = ""; + const currentFloors = this._currentFloors; + if (currentFloors.includes(toAdd)) { + return; + } + + this._updateFloors([...currentFloors, toAdd]); + } + + static override styles = css` + div { + margin-top: 8px; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-floors-picker": HaFloorsPicker; + } +} diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index e74631423171..7690c387adfd 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -87,8 +87,12 @@ export class HaAreaSelector extends LitElement { .label=${this.label} .helper=${this.helper} no-add - .deviceFilter=${this._filterDevices} - .entityFilter=${this._filterEntities} + .deviceFilter=${this.selector.area?.device + ? this._filterDevices + : undefined} + .entityFilter=${this.selector.area?.entity + ? this._filterEntities + : undefined} .disabled=${this.disabled} .required=${this.required} > @@ -102,8 +106,12 @@ export class HaAreaSelector extends LitElement { .helper=${this.helper} .pickAreaLabel=${this.label} no-add - .deviceFilter=${this._filterDevices} - .entityFilter=${this._filterEntities} + .deviceFilter=${this.selector.area?.device + ? this._filterDevices + : undefined} + .entityFilter=${this.selector.area?.entity + ? this._filterEntities + : undefined} .disabled=${this.disabled} .required=${this.required} > diff --git a/src/components/ha-selector/ha-selector-floor.ts b/src/components/ha-selector/ha-selector-floor.ts new file mode 100644 index 000000000000..eac63f414edb --- /dev/null +++ b/src/components/ha-selector/ha-selector-floor.ts @@ -0,0 +1,153 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { html, LitElement, PropertyValues, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { ensureArray } from "../../common/array/ensure-array"; +import type { DeviceRegistryEntry } from "../../data/device_registry"; +import { getDeviceIntegrationLookup } from "../../data/device_registry"; +import { fireEvent } from "../../common/dom/fire_event"; +import { + EntitySources, + fetchEntitySourcesWithCache, +} from "../../data/entity_sources"; +import type { FloorSelector } from "../../data/selector"; +import { + filterSelectorDevices, + filterSelectorEntities, +} from "../../data/selector"; +import { HomeAssistant } from "../../types"; +import "../ha-floor-picker"; +import "../ha-floors-picker"; + +@customElement("ha-selector-floor") +export class HaFloorSelector extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public selector!: FloorSelector; + + @property() public value?: any; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + @state() private _entitySources?: EntitySources; + + private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); + + private _hasIntegration(selector: FloorSelector) { + return ( + (selector.floor?.entity && + ensureArray(selector.floor.entity).some( + (filter) => filter.integration + )) || + (selector.floor?.device && + ensureArray(selector.floor.device).some((device) => device.integration)) + ); + } + + protected willUpdate(changedProperties: PropertyValues): void { + if (changedProperties.has("selector") && this.value !== undefined) { + if (this.selector.floor?.multiple && !Array.isArray(this.value)) { + this.value = [this.value]; + fireEvent(this, "value-changed", { value: this.value }); + } else if (!this.selector.floor?.multiple && Array.isArray(this.value)) { + this.value = this.value[0]; + fireEvent(this, "value-changed", { value: this.value }); + } + } + } + + protected updated(changedProperties: PropertyValues): void { + if ( + changedProperties.has("selector") && + this._hasIntegration(this.selector) && + !this._entitySources + ) { + fetchEntitySourcesWithCache(this.hass).then((sources) => { + this._entitySources = sources; + }); + } + } + + protected render() { + if (this._hasIntegration(this.selector) && !this._entitySources) { + return nothing; + } + + if (!this.selector.floor?.multiple) { + return html` + + `; + } + + return html` + + `; + } + + private _filterEntities = (entity: HassEntity): boolean => { + if (!this.selector.floor?.entity) { + return true; + } + + return ensureArray(this.selector.floor.entity).some((filter) => + filterSelectorEntities(filter, entity, this._entitySources) + ); + }; + + private _filterDevices = (device: DeviceRegistryEntry): boolean => { + if (!this.selector.floor?.device) { + return true; + } + + const deviceIntegrations = this._entitySources + ? this._deviceIntegrationLookup( + this._entitySources, + Object.values(this.hass.entities) + ) + : undefined; + + return ensureArray(this.selector.floor.device).some((filter) => + filterSelectorDevices(filter, device, deviceIntegrations) + ); + }; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-floor": HaFloorSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 5ab02782c180..e622721b6db5 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -30,6 +30,7 @@ const LOAD_ELEMENTS = { entity: () => import("./ha-selector-entity"), statistic: () => import("./ha-selector-statistic"), file: () => import("./ha-selector-file"), + floor: () => import("./ha-selector-floor"), label: () => import("./ha-selector-label"), language: () => import("./ha-selector-language"), navigation: () => import("./ha-selector-navigation"), diff --git a/src/data/selector.ts b/src/data/selector.ts index 442fba220f30..3abb7ae6fc71 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -31,6 +31,7 @@ export type Selector = | DateSelector | DateTimeSelector | DeviceSelector + | FloorSelector | LegacyDeviceSelector | DurationSelector | EntitySelector @@ -170,6 +171,14 @@ export interface DeviceSelector { } | null; } +export interface FloorSelector { + floor: { + entity?: EntitySelectorFilter | readonly EntitySelectorFilter[]; + device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[]; + multiple?: boolean; + } | null; +} + export interface LegacyDeviceSelector { device: DeviceSelector["device"] & { /** From a3024b38e97180b18550ccde94384926910aeccf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 10:44:13 +0200 Subject: [PATCH 11/14] fix floor icon color dark mode (#20310) --- src/panels/config/areas/dialog-floor-registry-detail.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/panels/config/areas/dialog-floor-registry-detail.ts b/src/panels/config/areas/dialog-floor-registry-detail.ts index 421c996b0c8f..775d18399382 100644 --- a/src/panels/config/areas/dialog-floor-registry-detail.ts +++ b/src/panels/config/areas/dialog-floor-registry-detail.ts @@ -213,6 +213,9 @@ class DialogFloorDetail extends LitElement { display: block; margin-bottom: 16px; } + ha-floor-icon { + color: var(--secondary-text-color); + } `, ]; } From 2e58d6656cc5495c7a22fe67a0d6c5f58a34832c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 11:23:32 +0200 Subject: [PATCH 12/14] Add drag and drop to area dashboard (#20289) * Add drag and drop to area dashboard * Update ha-config-areas-dashboard.ts * Fix unassign path * Add delay for touch * Update ha-config-areas-dashboard.ts --------- Co-authored-by: Paul Bottein --- .../config/areas/ha-config-areas-dashboard.ts | 65 ++++++++++++++++--- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index f4a38567d1b2..875e13367b4c 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -23,9 +23,11 @@ import "../../../components/ha-fab"; import "../../../components/ha-floor-icon"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; +import "../../../components/ha-sortable"; import { AreaRegistryEntry, createAreaRegistryEntry, + updateAreaRegistryEntry, } from "../../../data/area_registry"; import { FloorRegistryEntry, @@ -50,6 +52,10 @@ import { } from "./show-dialog-area-registry-detail"; import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail"; +const UNASSIGNED_PATH = ["__unassigned__"]; + +const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true }; + @customElement("ha-config-areas-dashboard") export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -187,13 +193,22 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { >
    -
    - ${floor.areas.map((area) => this._renderArea(area))} -
    + +
    + ${floor.areas.map((area) => this._renderArea(area))} +
    +
    ` )} ${areasAndFloors?.unassisgnedAreas.length - ? html`
    + ? html`

    ${this.hass.localize( @@ -201,11 +216,20 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { )}

    -
    - ${areasAndFloors?.unassisgnedAreas.map((area) => - this._renderArea(area) - )} -
    + +
    + ${areasAndFloors?.unassisgnedAreas.map((area) => + this._renderArea(area) + )} +
    +
    ` : nothing}
    @@ -281,6 +305,29 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { loadAreaRegistryDetailDialog(); } + private async _areaMoved(ev) { + const areasAndFloors = this._processAreas( + this.hass.areas, + this.hass.devices, + this.hass.entities, + this._floors! + ); + let area: AreaRegistryEntry; + if (ev.detail.oldPath === UNASSIGNED_PATH) { + area = areasAndFloors.unassisgnedAreas[ev.detail.oldIndex]; + } else { + const oldFloor = areasAndFloors.floors!.find( + (floor) => floor.floor_id === ev.detail.oldPath[0] + ); + area = oldFloor!.areas[ev.detail.oldIndex]; + } + + await updateAreaRegistryEntry(this.hass, area.area_id, { + floor_id: + ev.detail.newPath === UNASSIGNED_PATH ? null : ev.detail.newPath[0], + }); + } + private _handleFloorAction(ev: CustomEvent) { const floor = (ev.currentTarget as any).floor; switch (ev.detail.index) { From 4fb42d3545a37d1868d6e1a36a48f6d13efc9342 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 11:23:43 +0200 Subject: [PATCH 13/14] Fix and optimize automation overflow (#20293) * WIP fix and optimize automation overflow * finish * Prettier --------- Co-authored-by: Paul Bottein --- src/components/data-table/ha-data-table.ts | 10 +- .../add-automation-element-dialog.ts | 2 +- .../config/automation/ha-automation-picker.ts | 210 +++++++++++------- 3 files changed, 133 insertions(+), 89 deletions(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index e514526c3bd1..e0f7c8894e45 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -627,9 +627,13 @@ export class HaDataTable extends LitElement { ev .composedPath() .find((el) => - ["ha-checkbox", "mwc-button", "ha-button", "ha-assist-chip"].includes( - (el as HTMLElement).localName - ) + [ + "ha-checkbox", + "mwc-button", + "ha-button", + "ha-icon-button", + "ha-assist-chip", + ].includes((el as HTMLElement).localName) ) ) { return; diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 3222c1b1e221..09fcf360a1ad 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -556,7 +556,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog { > - ` + ` : ""} ${repeat( items, diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index c53a786621a3..b5208b944e3c 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -1,5 +1,6 @@ import { consume } from "@lit-labs/context"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; +import "@material/web/divider/divider"; import { mdiChevronRight, mdiCog, @@ -30,7 +31,7 @@ import { html, nothing, } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { computeCssColor } from "../../../common/color/compute-color"; @@ -58,6 +59,7 @@ import "../../../components/ha-filter-floor-areas"; import "../../../components/ha-filter-labels"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-overflow-menu"; +import "../../../components/ha-menu"; import "../../../components/ha-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; @@ -99,6 +101,7 @@ import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { configSections } from "../ha-panel-config"; import { showNewAutomationDialog } from "./show-dialog-new-automation"; +import type { HaMenu } from "../../../components/ha-menu"; type AutomationItem = AutomationEntity & { name: string; @@ -143,6 +146,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { @consume({ context: fullEntitiesContext, subscribe: true }) _entityReg!: EntityRegistryEntry[]; + @state() private _overflowAutomation?: AutomationItem; + + @query("#overflow-menu") private _overflowMenu!: HaMenu; + private _automations = memoizeOne( ( automations: AutomationEntity[], @@ -291,89 +298,33 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { columns.actions = { title: "", width: "64px", - type: "overflow-menu", + type: "icon-button", template: (automation) => html` - this._showInfo(automation), - }, - { - path: mdiCog, - label: this.hass.localize( - "ui.panel.config.automation.picker.show_settings" - ), - action: () => this._showSettings(automation), - }, - { - path: mdiTag, - label: this.hass.localize( - `ui.panel.config.automation.picker.${automation.category ? "edit_category" : "assign_category"}` - ), - action: () => this._editCategory(automation), - }, - { - path: mdiPlay, - label: this.hass.localize( - "ui.panel.config.automation.editor.run" - ), - action: () => this._runActions(automation), - }, - { - path: mdiTransitConnection, - label: this.hass.localize( - "ui.panel.config.automation.editor.show_trace" - ), - action: () => this._showTrace(automation), - }, - { - divider: true, - }, - { - path: mdiContentDuplicate, - label: this.hass.localize( - "ui.panel.config.automation.picker.duplicate" - ), - action: () => this.duplicate(automation), - }, - { - path: - automation.state === "off" - ? mdiPlayCircleOutline - : mdiStopCircleOutline, - label: - automation.state === "off" - ? this.hass.localize( - "ui.panel.config.automation.editor.enable" - ) - : this.hass.localize( - "ui.panel.config.automation.editor.disable" - ), - action: () => this._toggle(automation), - }, - { - label: this.hass.localize( - "ui.panel.config.automation.picker.delete" - ), - path: mdiDelete, - action: () => this._deleteConfirm(automation), - warning: true, - }, - ]} - > - + `, }; return columns; } ); + private _showOverflowMenu = (ev) => { + if ( + this._overflowMenu.open && + ev.target === this._overflowMenu.anchorElement + ) { + this._overflowMenu.close(); + return; + } + this._overflowAutomation = ev.target.automation; + this._overflowMenu.anchorElement = ev.target; + this._overflowMenu.show(); + }; + protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { return [ subscribeCategoryRegistry( @@ -689,6 +640,80 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { + + + +
    + ${this.hass.localize("ui.panel.config.automation.editor.show_info")} +
    +
    + + + +
    + ${this.hass.localize( + "ui.panel.config.automation.picker.show_settings" + )} +
    +
    + + +
    + ${this.hass.localize( + `ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}` + )} +
    +
    + + +
    + ${this.hass.localize("ui.panel.config.automation.editor.run")} +
    +
    + + +
    + ${this.hass.localize( + "ui.panel.config.automation.editor.show_trace" + )} +
    +
    + + + +
    + ${this.hass.localize("ui.panel.config.automation.picker.duplicate")} +
    +
    + + +
    + ${ + this._overflowAutomation?.state === "off" + ? this.hass.localize("ui.panel.config.automation.editor.enable") + : this.hass.localize( + "ui.panel.config.automation.editor.disable" + ) + } +
    +
    + + +
    + ${this.hass.localize("ui.panel.config.automation.picker.delete")} +
    +
    +
    `; } @@ -815,22 +840,29 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { this._applyFilters(); } - private _showInfo(automation: any) { + private _showInfo(ev) { + const automation = ev.currentTarget.parentElement.anchorElement.automation; fireEvent(this, "hass-more-info", { entityId: automation.entity_id }); } - private _showSettings(automation: any) { + private _showSettings(ev) { + const automation = ev.currentTarget.parentElement.anchorElement.automation; + fireEvent(this, "hass-more-info", { entityId: automation.entity_id, view: "settings", }); } - private _runActions(automation: any) { + private _runActions(ev) { + const automation = ev.currentTarget.parentElement.anchorElement.automation; + triggerAutomationActions(this.hass, automation.entity_id); } - private _editCategory(automation: any) { + private _editCategory(ev) { + const automation = ev.currentTarget.parentElement.anchorElement.automation; + const entityReg = this._entityReg.find( (reg) => reg.entity_id === automation.entity_id ); @@ -851,7 +883,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { }); } - private _showTrace(automation: any) { + private _showTrace(ev) { + const automation = ev.currentTarget.parentElement.anchorElement.automation; + if (!automation.attributes.id) { showAlertDialog(this, { text: this.hass.localize( @@ -865,14 +899,18 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { ); } - private async _toggle(automation): Promise { + private async _toggle(ev): Promise { + const automation = ev.currentTarget.parentElement.anchorElement.automation; + const service = automation.state === "off" ? "turn_on" : "turn_off"; await this.hass.callService("automation", service, { entity_id: automation.entity_id, }); } - private async _deleteConfirm(automation) { + private async _deleteConfirm(ev) { + const automation = ev.currentTarget.parentElement.anchorElement.automation; + showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.automation.picker.delete_confirm_title" @@ -906,7 +944,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { } } - private async duplicate(automation) { + private async _duplicate(ev) { + const automation = ev.currentTarget.parentElement.anchorElement.automation; + try { const config = await fetchAutomationFileConfig( this.hass, From 871949e7607eaa4faa63e605a2fd1ff59530cb57 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 11:41:16 +0200 Subject: [PATCH 14/14] Bumped version to 20240402.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 26cdafe44428..caf1247abc3b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20240329.1" +version = "20240402.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md"