mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-20 21:01:30 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb8d092ef1 | ||
|
|
382bf6389c | ||
|
|
897780c554 | ||
|
|
8e37e41d67 | ||
|
|
fb85c0029c |
No files matched your search
@@ -0,0 +1,95 @@
|
||||
import type {
|
||||
DataTableFiltersValue,
|
||||
DataTableFiltersValues,
|
||||
} from "../../data/data_table_filters";
|
||||
import { dataTableQueryParamConfig } from "./data-table-query-params";
|
||||
import {
|
||||
decodeQueryParams,
|
||||
hasQueryParams,
|
||||
type QueryParamConfig,
|
||||
type QueryParamValues,
|
||||
type SearchParamsSource,
|
||||
} from "./query-params";
|
||||
|
||||
// The filters of the helpers table. Search, sorting and grouping are kept by
|
||||
// the data table, see data-table-query-params.
|
||||
export const helpersFilterQueryParamConfig = {
|
||||
list: [
|
||||
"floor",
|
||||
"area",
|
||||
"device",
|
||||
"label",
|
||||
"category",
|
||||
"assistants",
|
||||
// Read until 2027.5, replaced with `assistants` on the next write
|
||||
"voice_assistant",
|
||||
],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
export type HelpersFilterQueryParams = QueryParamValues<
|
||||
typeof helpersFilterQueryParamConfig
|
||||
>;
|
||||
|
||||
const list = (value: DataTableFiltersValue): string[] | undefined =>
|
||||
Array.isArray(value) ? value : undefined;
|
||||
|
||||
const floorAreas = (
|
||||
value: DataTableFiltersValue
|
||||
): Record<string, string[]> | undefined =>
|
||||
value && !Array.isArray(value) ? value : undefined;
|
||||
|
||||
export const helpersFilterQueryParams = (
|
||||
filters: DataTableFiltersValues
|
||||
): HelpersFilterQueryParams => ({
|
||||
floor: floorAreas(filters["ha-filter-floor-areas"])?.floors,
|
||||
area: floorAreas(filters["ha-filter-floor-areas"])?.areas,
|
||||
device: list(filters["ha-filter-devices"]),
|
||||
label: list(filters["ha-filter-labels"]),
|
||||
category: list(filters["ha-filter-categories"]),
|
||||
assistants: list(filters["ha-filter-voice-assistants"]),
|
||||
});
|
||||
|
||||
// What a link says to show, undefined when it has no table params so what was
|
||||
// picked before stays. A link replaces the search and the filters, ones left
|
||||
// behind would hide part of what it shows. The table applies its sorting and
|
||||
// grouping.
|
||||
export interface HelpersLink {
|
||||
search: string;
|
||||
filters: DataTableFiltersValues;
|
||||
}
|
||||
|
||||
export const helpersLinkFromUrl = (
|
||||
searchParams: SearchParamsSource
|
||||
): HelpersLink | undefined => {
|
||||
if (
|
||||
!hasQueryParams(searchParams, helpersFilterQueryParamConfig) &&
|
||||
!hasQueryParams(searchParams, dataTableQueryParamConfig)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = decodeQueryParams(searchParams, helpersFilterQueryParamConfig);
|
||||
|
||||
const { search } = decodeQueryParams(searchParams, dataTableQueryParamConfig);
|
||||
|
||||
const values = (key: keyof HelpersFilterQueryParams): string[] =>
|
||||
params[key]?.filter(Boolean) ?? [];
|
||||
|
||||
const floors = values("floor");
|
||||
const areas = values("area");
|
||||
const assistants = values("assistants");
|
||||
|
||||
return {
|
||||
search: search ?? "",
|
||||
filters: {
|
||||
"ha-filter-floor-areas":
|
||||
floors.length || areas.length ? { floors, areas } : undefined,
|
||||
"ha-filter-devices": values("device"),
|
||||
"ha-filter-labels": values("label"),
|
||||
"ha-filter-categories": values("category"),
|
||||
"ha-filter-voice-assistants": assistants.length
|
||||
? assistants
|
||||
: values("voice_assistant"),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -16,20 +16,27 @@ import {
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { mainWindow } from "../../../common/dom/get_main_window";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { getHistoryState, navigate } from "../../../common/navigate";
|
||||
import { navigate, replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { currentPath } from "../../../common/url/current-path";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import {
|
||||
helpersFilterQueryParamConfig,
|
||||
helpersFilterQueryParams,
|
||||
helpersLinkFromUrl,
|
||||
} from "../../../common/url/helpers-query-params";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
@@ -107,6 +114,7 @@ import {
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
@@ -237,8 +245,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _diagnosticHandlers?: Record<string, boolean>;
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state()
|
||||
private _filters: DataTableFiltersValues = {};
|
||||
|
||||
@@ -267,10 +273,25 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _filteredHelperEntityIds?: string[] | null;
|
||||
|
||||
// Not cached, the table only renders once the helpers have loaded
|
||||
@query("hass-tabs-subpage-data-table")
|
||||
private _dataTable?: HaTabsSubpageDataTable;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
|
||||
// The filters are kept in the URL by the page, the search, sorting and
|
||||
// grouping by the table
|
||||
private _filterParams = new QueryParamsController(
|
||||
this,
|
||||
helpersFilterQueryParamConfig,
|
||||
{
|
||||
enabled: () => true,
|
||||
navigated: () => this._applyLink(),
|
||||
}
|
||||
);
|
||||
|
||||
private get _availableAssistants() {
|
||||
return getAvailableAssistants(this.cloudStatus, this.hass);
|
||||
}
|
||||
@@ -367,6 +388,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "type",
|
||||
},
|
||||
editable: getEditableTableColumn(
|
||||
localize,
|
||||
@@ -682,6 +704,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.activeFilters=${this._activeFilters}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@row-click=${this._openEditDialog}
|
||||
sync-url
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
has-fab
|
||||
@@ -854,9 +877,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
this._filters = { ...this._filters, [type]: ev.detail.value };
|
||||
this._filteredItems = { ...this._filteredItems, [type]: ev.detail.items };
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = this._filters;
|
||||
}
|
||||
this._storageFilters = this._filters;
|
||||
this._writeFilters();
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
@@ -916,16 +938,16 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
) {
|
||||
const categoryItems = new Set<string>();
|
||||
this._helperEntities
|
||||
.filter(
|
||||
(stateItem) =>
|
||||
filter[0] ===
|
||||
entityRegistryByEntityId(this._entityReg || [])[
|
||||
stateItem.entity_id
|
||||
]?.categories.helpers
|
||||
)
|
||||
.filter((stateItem) => {
|
||||
const category = entityRegistryByEntityId(this._entityReg || [])[
|
||||
stateItem.entity_id
|
||||
]?.categories.helpers;
|
||||
|
||||
return category !== undefined && filter.includes(category);
|
||||
})
|
||||
.forEach((stateItem) => categoryItems.add(stateItem.entity_id));
|
||||
(this._disabledEntityEntries || [])
|
||||
.filter((entry) => filter[0] === entry.categories.helpers)
|
||||
.filter((entry) => filter.includes(entry.categories.helpers))
|
||||
.forEach((entry) => categoryItems.add(entry.entity_id));
|
||||
if (!items) {
|
||||
items = categoryItems;
|
||||
@@ -972,63 +994,56 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
this._filteredHelperEntityIds = items ? [...items] : undefined;
|
||||
}
|
||||
|
||||
private _fromUrl = false;
|
||||
// The domain an add link asked for, started once the page has hass
|
||||
private _addDomain?: string;
|
||||
|
||||
public connectedCallback() {
|
||||
// Leave the add path before the URL is read, so the table params are kept
|
||||
// on the helpers page itself. The route isn't set yet on the first
|
||||
// connect, so this reads the URL. navigate() would only replace it after
|
||||
// the table params are written, and drop them.
|
||||
if (currentPath() === "/config/helpers/add") {
|
||||
const params = new URLSearchParams(mainWindow.location.search);
|
||||
this._addDomain = params.get("domain") ?? undefined;
|
||||
params.delete("domain");
|
||||
const queryString = params.toString() ? `?${params}` : "";
|
||||
replaceCurrentUrl(
|
||||
__DEMO__
|
||||
? `${mainWindow.location.pathname}${queryString}#/config/helpers`
|
||||
: `/config/helpers${queryString}`
|
||||
);
|
||||
fireEvent(mainWindow, "location-changed", { replace: true });
|
||||
// Opened once the table is there, see updated()
|
||||
this.requestUpdate();
|
||||
}
|
||||
super.connectedCallback();
|
||||
window.addEventListener("location-changed", this._locationChanged);
|
||||
window.addEventListener("popstate", this._popState);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("location-changed", this._locationChanged);
|
||||
window.removeEventListener("popstate", this._popState);
|
||||
// Links, like the ones on a device page, say in the URL what to show. The
|
||||
// router keeps the page open, so a link can land on it.
|
||||
private _applyLink() {
|
||||
const searchParams = this._filterParams.searchParams();
|
||||
const link = searchParams ? helpersLinkFromUrl(searchParams) : undefined;
|
||||
|
||||
if (link) {
|
||||
this._filter = link.search;
|
||||
this._filters = link.filters;
|
||||
this._storageFilters = link.filters;
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _locationChanged = () => {
|
||||
if (window.location.search.substring(1) !== this._searchParms.toString()) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
};
|
||||
|
||||
private _popState = () => {
|
||||
if (window.location.search.substring(1) !== this._searchParms.toString()) {
|
||||
this._searchParms = new URLSearchParams(window.location.search);
|
||||
this._setFiltersFromUrl();
|
||||
}
|
||||
};
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
const area = this._searchParms.get("area");
|
||||
const device = this._searchParms.get("device");
|
||||
const label = this._searchParms.get("label");
|
||||
const category = this._searchParms.get("category");
|
||||
const voiceAssistant = this._searchParms.get("voice_assistant");
|
||||
|
||||
if (!area && !category && !label && !device && !voiceAssistant) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fromUrl = true;
|
||||
this._filter = getHistoryState()?.filter || "";
|
||||
|
||||
this._filters = {
|
||||
"ha-filter-floor-areas": area ? { areas: [area] } : undefined,
|
||||
"ha-filter-devices": device ? [device] : [],
|
||||
"ha-filter-labels": label ? [label] : [],
|
||||
"ha-filter-categories": category ? [category] : [],
|
||||
"ha-filter-voice-assistants": voiceAssistant ? [voiceAssistant] : [],
|
||||
};
|
||||
private _writeFilters() {
|
||||
this._filterParams.write(helpersFilterQueryParams(this._filters));
|
||||
}
|
||||
|
||||
private _clearFilter() {
|
||||
this._filters = {};
|
||||
this._filteredItems = {};
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = {};
|
||||
}
|
||||
this._storageFilters = {};
|
||||
this._writeFilters();
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
@@ -1141,7 +1156,6 @@ ${rejected
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._setFiltersFromUrl();
|
||||
this._fetchEntitySources();
|
||||
|
||||
if (isComponentLoaded(this.hass.config, "diagnostics")) {
|
||||
@@ -1153,9 +1167,13 @@ ${rejected
|
||||
this._diagnosticHandlers = handlers;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.route.path === "/add") {
|
||||
this._handleAdd();
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (this._addDomain && this._dataTable) {
|
||||
this._handleAdd(this._dataTable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1194,12 +1212,18 @@ ${rejected
|
||||
this._entitySource = entityDomains;
|
||||
}
|
||||
|
||||
private async _handleAdd() {
|
||||
const domain = extractSearchParam("domain");
|
||||
navigate("/config/helpers", { replace: true });
|
||||
private async _handleAdd(dataTable: HaTabsSubpageDataTable) {
|
||||
const domain = this._addDomain;
|
||||
this._addDomain = undefined;
|
||||
if (!domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The dialog adds a history entry. Let the table write its search,
|
||||
// sorting and grouping first, or they would only be on that entry and
|
||||
// be lost when it closes.
|
||||
await dataTable.updateComplete;
|
||||
|
||||
if (isHelperDomain(domain)) {
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
@@ -1254,7 +1278,7 @@ ${rejected
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._filters = this._storageFilters;
|
||||
this._setFiltersFromUrl();
|
||||
this._applyLink();
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -31,6 +31,20 @@ const marketplaceTableParams = {
|
||||
direction: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
// The helpers table reads these from the link
|
||||
const helpersTableParams = {
|
||||
search: "string?",
|
||||
sort: "string?",
|
||||
direction: "string?",
|
||||
group: "string?",
|
||||
floor: "string?",
|
||||
area: "string?",
|
||||
device: "string?",
|
||||
label: "string?",
|
||||
category: "string?",
|
||||
assistants: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
export const getMyRedirects = (): Redirects => ({
|
||||
application_credentials: {
|
||||
redirect: "/config/application_credentials",
|
||||
@@ -259,6 +273,7 @@ export const getMyRedirects = (): Redirects => ({
|
||||
},
|
||||
helpers: {
|
||||
redirect: "/config/helpers",
|
||||
params: helpersTableParams,
|
||||
},
|
||||
labs: {
|
||||
redirect: "/config/labs",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
helpersFilterQueryParamConfig,
|
||||
helpersFilterQueryParams,
|
||||
helpersLinkFromUrl,
|
||||
} from "../../../src/common/url/helpers-query-params";
|
||||
import {
|
||||
createQueryString,
|
||||
updateQueryString,
|
||||
} from "../../../src/common/url/query-params";
|
||||
|
||||
describe("helpers query params", () => {
|
||||
it("reads nothing without table params", () => {
|
||||
expect(helpersLinkFromUrl("?domain=input_boolean")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("replaces every filter and the search with what a link asks for", () => {
|
||||
expect(helpersLinkFromUrl("?category=abc&label=a,b")).toEqual({
|
||||
search: "",
|
||||
filters: {
|
||||
"ha-filter-floor-areas": undefined,
|
||||
"ha-filter-devices": [],
|
||||
"ha-filter-labels": ["a", "b"],
|
||||
"ha-filter-categories": ["abc"],
|
||||
"ha-filter-voice-assistants": [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("still reads voice_assistant, with assistants winning", () => {
|
||||
expect(
|
||||
helpersLinkFromUrl("?voice_assistant=conversation")?.filters[
|
||||
"ha-filter-voice-assistants"
|
||||
]
|
||||
).toEqual(["conversation"]);
|
||||
expect(
|
||||
helpersLinkFromUrl("?voice_assistant=conversation&assistants=cloud.alexa")
|
||||
?.filters["ha-filter-voice-assistants"]
|
||||
).toEqual(["cloud.alexa"]);
|
||||
});
|
||||
|
||||
it("replaces voice_assistant with assistants on write", () => {
|
||||
const url = "?historyBack=1&voice_assistant=conversation";
|
||||
const link = helpersLinkFromUrl(url);
|
||||
|
||||
expect(
|
||||
updateQueryString(
|
||||
url,
|
||||
helpersFilterQueryParams(link?.filters ?? {}),
|
||||
helpersFilterQueryParamConfig
|
||||
)
|
||||
).toBe("historyBack=1&assistants=conversation");
|
||||
});
|
||||
|
||||
it("round trips the filters", () => {
|
||||
const filters = {
|
||||
"ha-filter-floor-areas": { floors: ["ground"], areas: ["kitchen"] },
|
||||
"ha-filter-devices": ["abc"],
|
||||
"ha-filter-labels": ["a"],
|
||||
"ha-filter-categories": ["cat"],
|
||||
"ha-filter-voice-assistants": ["conversation"],
|
||||
};
|
||||
|
||||
expect(
|
||||
helpersLinkFromUrl(
|
||||
createQueryString(
|
||||
helpersFilterQueryParams(filters),
|
||||
helpersFilterQueryParamConfig
|
||||
)
|
||||
)
|
||||
).toEqual({ search: "", filters });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user