From 46ac9c2a81cd43343b125169c58150102bbb5626 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 15 Apr 2026 16:00:49 +0100 Subject: [PATCH 1/3] Update filter UI --- src/dashboards/hacs-dashboard.ts | 128 +++++++++++++------------------ 1 file changed, 55 insertions(+), 73 deletions(-) diff --git a/src/dashboards/hacs-dashboard.ts b/src/dashboards/hacs-dashboard.ts index a42c4bb4..ce3cf9c9 100644 --- a/src/dashboards/hacs-dashboard.ts +++ b/src/dashboards/hacs-dashboard.ts @@ -25,15 +25,13 @@ import type { } from "../../homeassistant-frontend/src/components/data-table/ha-data-table"; import "../../homeassistant-frontend/src/layouts/hass-tabs-subpage-data-table"; -import "../../homeassistant-frontend/src/components/ha-button-menu"; import "../../homeassistant-frontend/src/components/ha-fab"; -import "../../homeassistant-frontend/src/components/ha-form/ha-form"; +import "../../homeassistant-frontend/src/components/ha-filter-states"; import "../../homeassistant-frontend/src/components/ha-markdown"; import "../../homeassistant-frontend/src/components/ha-menu"; import "../../homeassistant-frontend/src/components/ha-md-menu-item"; import { LocalizeFunc } from "../../homeassistant-frontend/src/common/translations/localize"; -import { HaFormSchema } from "../../homeassistant-frontend/src/components/ha-form/types"; import { HaMenu } from "../../homeassistant-frontend/src/components/ha-menu"; import "../../homeassistant-frontend/src/components/ha-svg-icon"; import { PageNavigation } from "../../homeassistant-frontend/src/layouts/hass-tabs-subpage"; @@ -105,6 +103,8 @@ export class HacsDashboard extends LitElement { @storage({ key: "hacs-dashboard-table-columns-ordering", state: true, subscribe: false }) private _orderTableColumns?: string[]; + @state() private _expandedFilter?: string; + @query("#overflow-menu") private _overflowMenu!: HaMenu; @@ -159,18 +159,38 @@ export class HacsDashboard extends LitElement { @click=${this._showOverflowMenu} > - filter.startsWith("status_")) || "", - type: this._activeFilters?.find((filter) => filter.startsWith("type_")) || "", - }} - .schema=${this._filterSchema(this.hacs.localize, this.hacs.info.categories)} - .computeLabel=${this._computeFilterFormLabel} - @value-changed=${this._handleFilterChanged} - > + id="status-filter" + .label=${this.hacs.localize("dialog_overview.status")} + .value=${this._activeFilters?.filter((filter) => filter.startsWith("status_"))} + .states=${STATUS_ORDER.map((filter) => ({ + value: `status_${filter}`, + label: + this.hacs.localize( + // @ts-ignore + `repository_status.${filter}`, + ) || filter, + }))} + @data-table-filter-changed=${this._handlePaneFilterChanged} + .expanded=${this._expandedFilter === "status-filter"} + @expanded-changed=${this._handleFilterExpanded} + > + filter.startsWith("type_"))} + .states=${this.hacs.info.categories.map((type: string) => ({ + value: `type_${type}`, + label: this.hacs.localize(`common.type.${type as RepositoryType}`), + }))} + @data-table-filter-changed=${this._handlePaneFilterChanged} + .expanded=${this._expandedFilter === "type-filter"} + @expanded-changed=${this._handleFilterExpanded} + > ${this._overflowMenuRepository @@ -463,6 +483,15 @@ export class HacsDashboard extends LitElement { this._overflowMenu.show(); }; + private _handleFilterExpanded(ev) { + const filterId = (ev.target as HTMLElement).id; + if (ev.detail.expanded) { + this._expandedFilter = filterId; + } else if (this._expandedFilter === filterId) { + this._expandedFilter = undefined; + } + } + private _groupOrder = memoize( (localize: LocalizeFunc, activeGrouping: string | undefined) => activeGrouping === "translated_status" @@ -475,46 +504,6 @@ export class HacsDashboard extends LitElement { : undefined, ); - private _filterSchema = memoize( - (localizeFunc: LocalizeFunc, types: string[]) => - [ - { - name: "filters", - type: "constant", - value: "", - }, - { - name: "status", - selector: { - select: { - options: STATUS_ORDER.map((filter) => ({ - value: `status_${filter}`, - label: localizeFunc( - // @ts-ignore - `repository_status.${filter}`, - ), - })), - mode: "dropdown", - sort: false, - }, - }, - }, - { - name: "type", - selector: { - select: { - options: types.map((type: string) => ({ - label: localizeFunc(`common.type.${type as RepositoryType}`), - value: `type_${type}`, - })), - mode: "dropdown", - sort: true, - }, - }, - }, - ] as const satisfies readonly HaFormSchema[], - ); - get _scrollerTarget() { return ( this.shadowRoot @@ -529,31 +518,24 @@ export class HacsDashboard extends LitElement { ); } - private _computeFilterFormLabel = (schema, _) => - this.hacs.localize( - // @ts-ignore - `dialog_overview.${schema.name}`, - ) || - this.hacs.localize( - // @ts-ignore - `dialog_overview.sections.${schema.name}`, - ) || - schema.name; - private _handleRowClicked(ev: CustomEvent) { navigate(`/hacs/repository/${ev.detail.id}`); } - private _handleFilterChanged(ev: CustomEvent) { + private _handlePaneFilterChanged(ev: CustomEvent) { ev.stopPropagation(); - const data = ev.detail.value; - const updatedFilters: string[] = Object.entries(data) - .filter( - ([key, value]) => - ["status", "type"].includes(key) && ![undefined, null, ""].includes(value), - ) - .map(([_, value]) => value); - this._activeFilters = updatedFilters.length ? updatedFilters : undefined; + + const filterId = (ev.target as HTMLElement).id; + const prefix = + filterId === "status-filter" + ? "status_" + : "type_"; + + const preservedFilters = (this._activeFilters || []).filter((filter) => !filter.startsWith(prefix)); + const nextValues = ev.detail.value || []; + const nextFilters = [...preservedFilters, ...nextValues]; + + this._activeFilters = nextFilters.length ? nextFilters : undefined; } private _handleSearchFilterChanged(ev: CustomEvent) { From 317ae8e94342a8a2d365fe955767376e0c14cba7 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 15 Apr 2026 16:05:55 +0100 Subject: [PATCH 2/3] Typing --- src/dashboards/hacs-dashboard.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/dashboards/hacs-dashboard.ts b/src/dashboards/hacs-dashboard.ts index ce3cf9c9..da2d6ad8 100644 --- a/src/dashboards/hacs-dashboard.ts +++ b/src/dashboards/hacs-dashboard.ts @@ -36,7 +36,7 @@ import { HaMenu } from "../../homeassistant-frontend/src/components/ha-menu"; import "../../homeassistant-frontend/src/components/ha-svg-icon"; import { PageNavigation } from "../../homeassistant-frontend/src/layouts/hass-tabs-subpage"; import { haStyle } from "../../homeassistant-frontend/src/resources/styles"; -import type { HomeAssistant, Route } from "../../homeassistant-frontend/src/types"; +import type { HomeAssistant, Route, ValueChangedEvent } from "../../homeassistant-frontend/src/types"; import { brandsUrl } from "../../homeassistant-frontend/src/util/brands-url"; import { showHacsCustomRepositoriesDialog, @@ -483,8 +483,8 @@ export class HacsDashboard extends LitElement { this._overflowMenu.show(); }; - private _handleFilterExpanded(ev) { - const filterId = (ev.target as HTMLElement).id; + private _handleFilterExpanded(ev: CustomEvent<{ expanded: boolean }>) { + const filterId = (ev.currentTarget as HTMLElement).id; if (ev.detail.expanded) { this._expandedFilter = filterId; } else if (this._expandedFilter === filterId) { @@ -522,14 +522,11 @@ export class HacsDashboard extends LitElement { navigate(`/hacs/repository/${ev.detail.id}`); } - private _handlePaneFilterChanged(ev: CustomEvent) { + private _handlePaneFilterChanged(ev: ValueChangedEvent) { ev.stopPropagation(); - const filterId = (ev.target as HTMLElement).id; - const prefix = - filterId === "status-filter" - ? "status_" - : "type_"; + const filterId = (ev.currentTarget as HTMLElement).id; + const prefix = filterId === "status-filter" ? "status_" : "type_"; const preservedFilters = (this._activeFilters || []).filter((filter) => !filter.startsWith(prefix)); const nextValues = ev.detail.value || []; From b7724f94465837dbc65bf9f909acc545e76d74c3 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 15 Apr 2026 16:13:19 +0100 Subject: [PATCH 3/3] Better types --- src/dashboards/hacs-dashboard.ts | 43 ++++++++++++++++---------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/dashboards/hacs-dashboard.ts b/src/dashboards/hacs-dashboard.ts index da2d6ad8..55f8c507 100644 --- a/src/dashboards/hacs-dashboard.ts +++ b/src/dashboards/hacs-dashboard.ts @@ -60,7 +60,13 @@ const defaultKeyData = { hidden: true, }; -const STATUS_ORDER = ["pending-restart", "pending-upgrade", "installed", "new", "default"]; +type RepositoryStatus = RepositoryBase["status"]; +type RepositoryStatusKey = `repository_status.${RepositoryStatus}`; + +const STATUS_ORDER = ["pending-restart", "pending-upgrade", "installed", "new", "default"] as const satisfies readonly RepositoryStatus[]; + +const repositoryStatusKey = (status: RepositoryStatus): RepositoryStatusKey => + `repository_status.${status}`; const TABS: PageNavigation[] = [ { @@ -167,11 +173,7 @@ export class HacsDashboard extends LitElement { .value=${this._activeFilters?.filter((filter) => filter.startsWith("status_"))} .states=${STATUS_ORDER.map((filter) => ({ value: `status_${filter}`, - label: - this.hacs.localize( - // @ts-ignore - `repository_status.${filter}`, - ) || filter, + label: this.hacs.localize(repositoryStatusKey(filter)) || filter, }))} @data-table-filter-changed=${this._handlePaneFilterChanged} .expanded=${this._expandedFilter === "status-filter"} @@ -323,8 +325,7 @@ export class HacsDashboard extends LitElement { }) .map((repository) => ({ ...repository, - translated_status: - localizeFunc(`repository_status.${repository.status}`) || repository.status, + translated_status: localizeFunc(repositoryStatusKey(repository.status)) || repository.status, translated_category: localizeFunc(`common.type.${repository.category}`), })), ); @@ -495,27 +496,27 @@ export class HacsDashboard extends LitElement { private _groupOrder = memoize( (localize: LocalizeFunc, activeGrouping: string | undefined) => activeGrouping === "translated_status" - ? STATUS_ORDER.map((filter) => - localize( - // @ts-ignore - `repository_status.${filter}`, - ), - ) + ? STATUS_ORDER.map((filter) => localize(repositoryStatusKey(filter))) : undefined, ); get _scrollerTarget() { - return ( + const slot = this.shadowRoot ?.querySelector("hass-tabs-subpage-data-table") ?.shadowRoot?.querySelector("hass-tabs-subpage") ?.shadowRoot?.querySelector(".content") - ?.querySelectorAll("SLOT")[0] - // @ts-ignore - ?.assignedNodes() - ?.find((node) => node.nodeName === "HA-DATA-TABLE") - ?.shadowRoot?.querySelector(".scroller") - ); + ?.querySelector("slot") || null; + + if (!(slot instanceof HTMLSlotElement)) { + return undefined; + } + + const dataTable = slot + .assignedNodes() + .find((node): node is HTMLElement => node instanceof HTMLElement && node.nodeName === "HA-DATA-TABLE"); + + return dataTable?.shadowRoot?.querySelector(".scroller"); } private _handleRowClicked(ev: CustomEvent) {