mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-23 06:04:22 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2eca29c60 | ||
|
|
df3c44bbcf |
No files matched your search
@@ -0,0 +1,123 @@
|
||||
import type {
|
||||
DataTableFilter,
|
||||
DataTableFilters,
|
||||
} from "../../data/data_table_filters";
|
||||
import { dataTableQueryParamConfig } from "./data-table-query-params";
|
||||
import {
|
||||
createQueryString,
|
||||
decodeQueryParams,
|
||||
hasQueryParams,
|
||||
type QueryParamConfig,
|
||||
type QueryParamValues,
|
||||
type SearchParamsSource,
|
||||
} from "./query-params";
|
||||
|
||||
// The filters of the automations table. Search, sorting and grouping are kept
|
||||
// by the data table, see data-table-query-params.
|
||||
export const automationsFilterQueryParamConfig = {
|
||||
list: [
|
||||
"floor",
|
||||
"area",
|
||||
"device",
|
||||
"entity",
|
||||
"label",
|
||||
"category",
|
||||
"assistants",
|
||||
"blueprint",
|
||||
],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
export type AutomationsFilterQueryParams = QueryParamValues<
|
||||
typeof automationsFilterQueryParamConfig
|
||||
>;
|
||||
|
||||
const list = (filter: DataTableFilter | undefined): string[] | undefined =>
|
||||
Array.isArray(filter?.value) ? filter.value : undefined;
|
||||
|
||||
const floorAreas = (
|
||||
filter: DataTableFilter | undefined
|
||||
): Record<string, string[]> | undefined =>
|
||||
filter?.value && !Array.isArray(filter.value) ? filter.value : undefined;
|
||||
|
||||
export const automationsFilterQueryParams = (
|
||||
filters: DataTableFilters
|
||||
): AutomationsFilterQueryParams => ({
|
||||
floor: floorAreas(filters["ha-filter-floor-areas"])?.floors,
|
||||
area: floorAreas(filters["ha-filter-floor-areas"])?.areas,
|
||||
device: list(filters["ha-filter-devices"]),
|
||||
entity: list(filters["ha-filter-entities"]),
|
||||
label: list(filters["ha-filter-labels"]),
|
||||
category: list(filters["ha-filter-categories"]),
|
||||
assistants: list(filters["ha-filter-voice-assistants"]),
|
||||
blueprint: list(filters["ha-filter-blueprints"]),
|
||||
});
|
||||
|
||||
// The filters as the URL holds them, so values that filter the same, such as
|
||||
// no list and an empty one, or a list in another order, compare equal. A
|
||||
// filter list sends back what it was given in its own order.
|
||||
export const automationsFilterQueryString = (
|
||||
filters: DataTableFilters
|
||||
): string => {
|
||||
const params = automationsFilterQueryParams(filters);
|
||||
const sorted: AutomationsFilterQueryParams = {};
|
||||
|
||||
for (const key of automationsFilterQueryParamConfig.list) {
|
||||
sorted[key] = params[key] && [...params[key]].sort();
|
||||
}
|
||||
|
||||
return createQueryString(sorted, automationsFilterQueryParamConfig);
|
||||
};
|
||||
|
||||
// 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. The filters work out their items from the values.
|
||||
export interface AutomationsLink {
|
||||
search: string;
|
||||
filters: DataTableFilters;
|
||||
}
|
||||
|
||||
export const automationsLinkFromUrl = (
|
||||
searchParams: SearchParamsSource
|
||||
): AutomationsLink | undefined => {
|
||||
if (
|
||||
!hasQueryParams(searchParams, automationsFilterQueryParamConfig) &&
|
||||
!hasQueryParams(searchParams, dataTableQueryParamConfig)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = decodeQueryParams(
|
||||
searchParams,
|
||||
automationsFilterQueryParamConfig
|
||||
);
|
||||
|
||||
const { search } = decodeQueryParams(searchParams, dataTableQueryParamConfig);
|
||||
|
||||
const values = (key: keyof AutomationsFilterQueryParams): string[] =>
|
||||
params[key]?.filter(Boolean) ?? [];
|
||||
|
||||
const filter = (value: DataTableFilter["value"]): DataTableFilter => ({
|
||||
value,
|
||||
items: undefined,
|
||||
});
|
||||
|
||||
const floors = values("floor");
|
||||
const areas = values("area");
|
||||
|
||||
return {
|
||||
search: search ?? "",
|
||||
filters: {
|
||||
"ha-filter-floor-areas": filter(
|
||||
floors.length || areas.length ? { floors, areas } : undefined
|
||||
),
|
||||
"ha-filter-devices": filter(values("device")),
|
||||
"ha-filter-entities": filter(values("entity")),
|
||||
"ha-filter-labels": filter(values("label")),
|
||||
// The category filter picks one category
|
||||
"ha-filter-categories": filter(values("category").slice(0, 1)),
|
||||
"ha-filter-voice-assistants": filter(values("assistants")),
|
||||
"ha-filter-blueprints": filter(values("blueprint")),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -26,6 +26,8 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DataTableSettingsController } from "../../../common/controllers/data-table-settings-controller";
|
||||
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";
|
||||
@@ -34,6 +36,13 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
automationsFilterQueryParamConfig,
|
||||
automationsFilterQueryParams,
|
||||
automationsFilterQueryString,
|
||||
automationsLinkFromUrl,
|
||||
} from "../../../common/url/automations-query-params";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
@@ -43,7 +52,6 @@ import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-button";
|
||||
@@ -83,7 +91,10 @@ import {
|
||||
} from "../../../data/category_registry";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import { fullEntitiesContext, labelsContext } from "../../../data/context";
|
||||
import type { DataTableFilters } from "../../../data/data_table_filters";
|
||||
import type {
|
||||
DataTableFilter,
|
||||
DataTableFilters,
|
||||
} from "../../../data/data_table_filters";
|
||||
import {
|
||||
deserializeFilters,
|
||||
isFilterUsed,
|
||||
@@ -99,7 +110,6 @@ import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry"
|
||||
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
|
||||
import type { LabelRegistryEntry } from "../../../data/label/label_registry";
|
||||
import { createLabelRegistryEntry } from "../../../data/label/label_registry";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -177,18 +187,17 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public automations!: AutomationEntity[];
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@state() private _filteredEntityIds?: string[] | null;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "automation-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
private _tableSettings = new DataTableSettingsController(
|
||||
this,
|
||||
{
|
||||
search: "automation-table-search",
|
||||
sorting: "automation-table-sort",
|
||||
grouping: "automation-table-grouping",
|
||||
},
|
||||
{ defaultGrouping: "category" }
|
||||
);
|
||||
|
||||
@state()
|
||||
private _filters: DataTableFilters = {};
|
||||
@@ -203,7 +212,16 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _storageFilters: DataTableFilters = {};
|
||||
|
||||
private _fromUrl = false;
|
||||
// The filters are kept in the URL by the page, the search, sorting and
|
||||
// grouping by the table
|
||||
private _filterParams = new QueryParamsController(
|
||||
this,
|
||||
automationsFilterQueryParamConfig,
|
||||
{
|
||||
enabled: () => true,
|
||||
navigated: () => this._applyLink(),
|
||||
}
|
||||
);
|
||||
|
||||
@state() private _expandedFilter?: string;
|
||||
|
||||
@@ -222,12 +240,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _overflowAutomation?: AutomationItem;
|
||||
|
||||
@storage({ key: "automation-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "automation-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "automation-table-collapsed",
|
||||
state: false,
|
||||
@@ -399,6 +411,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
maxWidth: "82px",
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
urlName: "state",
|
||||
type: "overflow",
|
||||
title: this.hass.localize("ui.panel.config.automation.picker.state"),
|
||||
template: (automation) =>
|
||||
@@ -522,14 +535,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
).length
|
||||
}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize, automations)}
|
||||
.initialGroupColumn=${this._activeGrouping ?? "category"}
|
||||
.initialGroupColumn=${this._tableSettings.grouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.initialSorting=${this._tableSettings.sorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@sorting-changed=${this._tableSettings.sortingChanged}
|
||||
@grouping-changed=${this._tableSettings.groupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.data=${automations}
|
||||
.empty=${!this.automations.length}
|
||||
@@ -538,8 +551,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.no_automations"
|
||||
)}
|
||||
@clear-filter=${this._clearFilter}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
.filter=${this._tableSettings.search}
|
||||
@search-changed=${this._tableSettings.searchChanged}
|
||||
sync-url
|
||||
has-fab
|
||||
clickable
|
||||
class=${this.narrow ? "narrow" : ""}
|
||||
@@ -846,29 +860,57 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (!this.hasUpdated) {
|
||||
const hasUrlFilter =
|
||||
this._searchParms.has("area") ||
|
||||
this._searchParms.has("blueprint") ||
|
||||
this._searchParms.has("device") ||
|
||||
this._searchParms.has("label");
|
||||
if (!hasUrlFilter) {
|
||||
this._filters = this._storageFilters;
|
||||
}
|
||||
if (this._searchParms.has("area")) {
|
||||
this._filterArea();
|
||||
}
|
||||
if (this._searchParms.has("device")) {
|
||||
this._filterDevice();
|
||||
}
|
||||
if (this._searchParms.has("blueprint")) {
|
||||
this._filterBlueprint();
|
||||
}
|
||||
if (this._searchParms.has("label")) {
|
||||
this._filterLabel();
|
||||
}
|
||||
this._filters = this._storageFilters;
|
||||
this._applyLink();
|
||||
}
|
||||
}
|
||||
|
||||
// Links, like the ones on a device or area 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
|
||||
? automationsLinkFromUrl(searchParams)
|
||||
: undefined;
|
||||
|
||||
// Shown, but not stored as the user's choice
|
||||
if (link) {
|
||||
this._tableSettings.showSearch(link.search);
|
||||
this._filters = this._keepItems(link.filters);
|
||||
this._applyFilters();
|
||||
} else if (searchParams?.toString() === "") {
|
||||
// Opened again without params, like from the sidebar. Written once the
|
||||
// table has seen the URL without params, so it restores what it stored
|
||||
this._filters = this._keepItems(this._storageFilters);
|
||||
this._applyFilters();
|
||||
queueMicrotask(() => this._writeFilters());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
// The filters only work out their items again when their value changes, so
|
||||
// keep the items of the ones that stay as they are
|
||||
private _keepItems(filters: DataTableFilters): DataTableFilters {
|
||||
return Object.fromEntries(
|
||||
Object.entries(filters).map(([key, filter]) => {
|
||||
const current = this._filters[key];
|
||||
|
||||
return [
|
||||
key,
|
||||
current && deepEqual(filter.value, current.value) ? current : filter,
|
||||
];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _writeFilters() {
|
||||
this._filterParams.write(automationsFilterQueryParams(this._filters));
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
// The filtered list holds entity IDs, so also refresh it when automations
|
||||
@@ -888,30 +930,31 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _labelClicked = (ev: CustomEvent) => {
|
||||
const label = ev.detail.label;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-labels": {
|
||||
value: [label.label_id],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = this._filters;
|
||||
}
|
||||
this._applyFilters();
|
||||
this._setFilter("ha-filter-labels", {
|
||||
value: [label.label_id],
|
||||
items: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
private _filterChanged(ev) {
|
||||
this._setFilter(ev.target.localName, ev.detail);
|
||||
}
|
||||
|
||||
private _filterChanged(ev) {
|
||||
const type = ev.target.localName;
|
||||
this._filters = { ...this._filters, [type]: ev.detail };
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = this._filters;
|
||||
private _setFilter(type: string, filter: DataTableFilter) {
|
||||
const filters = { ...this._filters, [type]: filter };
|
||||
|
||||
// A filter also sends the value it was given, once it has found its items.
|
||||
// Only a change to what the URL holds is the user's, so a link isn't stored.
|
||||
if (
|
||||
automationsFilterQueryString(filters) !==
|
||||
automationsFilterQueryString(this._filters)
|
||||
) {
|
||||
this._storageFilters = { ...this._storageFilters, [type]: filter };
|
||||
}
|
||||
|
||||
this._filters = filters;
|
||||
this._applyFilters();
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _applyFilters() {
|
||||
@@ -961,81 +1004,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._filteredEntityIds = filteredEntityIds;
|
||||
}
|
||||
|
||||
private _filterArea() {
|
||||
const area = this._searchParms.get("area");
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-floor-areas": {
|
||||
value: { areas: [area] },
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _filterDevice() {
|
||||
const device = this._searchParms.get("device");
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-devices": {
|
||||
value: [device],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _filterLabel() {
|
||||
const label = this._searchParms.get("label");
|
||||
if (!label) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-labels": {
|
||||
value: [label],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private async _filterBlueprint() {
|
||||
const blueprint = this._searchParms.get("blueprint");
|
||||
if (!blueprint) {
|
||||
return;
|
||||
}
|
||||
this._fromUrl = true;
|
||||
const related = await findRelated(
|
||||
this.hass,
|
||||
"automation_blueprint",
|
||||
blueprint
|
||||
);
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-blueprints": {
|
||||
value: [blueprint],
|
||||
items: new Set(related.automation || []),
|
||||
},
|
||||
};
|
||||
this._applyFilters();
|
||||
}
|
||||
|
||||
private _clearFilter() {
|
||||
this._filters = {};
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = {};
|
||||
}
|
||||
this._storageFilters = {};
|
||||
this._applyFilters();
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _handleOverflowAction = (ev: HaDropdownSelectEvent) => {
|
||||
@@ -1589,14 +1562,6 @@ ${rejected
|
||||
}
|
||||
};
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value ?? "";
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -124,6 +124,22 @@ const tagsTableParams = {
|
||||
direction: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
// The automations table reads these from the link
|
||||
const automationsTableParams = {
|
||||
search: "string?",
|
||||
sort: "string?",
|
||||
direction: "string?",
|
||||
group: "string?",
|
||||
floor: "string?",
|
||||
area: "string?",
|
||||
device: "string?",
|
||||
entity: "string?",
|
||||
label: "string?",
|
||||
category: "string?",
|
||||
assistants: "string?",
|
||||
blueprint: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
export const getMyRedirects = (): Redirects => ({
|
||||
application_credentials: {
|
||||
redirect: "/config/application_credentials",
|
||||
@@ -354,6 +370,7 @@ export const getMyRedirects = (): Redirects => ({
|
||||
automations: {
|
||||
component: "automation",
|
||||
redirect: "/config/automation/dashboard",
|
||||
params: automationsTableParams,
|
||||
},
|
||||
scenes: {
|
||||
component: "scene",
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
automationsFilterQueryParamConfig,
|
||||
automationsFilterQueryParams,
|
||||
automationsFilterQueryString,
|
||||
automationsLinkFromUrl,
|
||||
} from "../../../src/common/url/automations-query-params";
|
||||
import { createQueryString } from "../../../src/common/url/query-params";
|
||||
|
||||
const filter = (value: string[] | Record<string, string[]> | undefined) => ({
|
||||
value,
|
||||
items: undefined,
|
||||
});
|
||||
|
||||
describe("automations query params", () => {
|
||||
it("reads nothing without table params", () => {
|
||||
expect(automationsLinkFromUrl("?historyBack=1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("replaces every filter and the search with what a link asks for", () => {
|
||||
expect(automationsLinkFromUrl("?device=abc")).toEqual({
|
||||
search: "",
|
||||
filters: {
|
||||
"ha-filter-floor-areas": filter(undefined),
|
||||
"ha-filter-devices": filter(["abc"]),
|
||||
"ha-filter-entities": filter([]),
|
||||
"ha-filter-labels": filter([]),
|
||||
"ha-filter-categories": filter([]),
|
||||
"ha-filter-voice-assistants": filter([]),
|
||||
"ha-filter-blueprints": filter([]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("round trips the filters", () => {
|
||||
const filters = {
|
||||
"ha-filter-floor-areas": filter({ floors: ["ground"], areas: ["hall"] }),
|
||||
"ha-filter-devices": filter(["a", "b"]),
|
||||
"ha-filter-entities": filter(["light.hall"]),
|
||||
"ha-filter-labels": filter(["test"]),
|
||||
"ha-filter-categories": filter(["cat"]),
|
||||
"ha-filter-voice-assistants": filter(["conversation"]),
|
||||
"ha-filter-blueprints": filter(["motion_light.yaml"]),
|
||||
};
|
||||
|
||||
expect(
|
||||
automationsLinkFromUrl(
|
||||
createQueryString(
|
||||
automationsFilterQueryParams(filters),
|
||||
automationsFilterQueryParamConfig
|
||||
)
|
||||
)
|
||||
).toEqual({ search: "", filters });
|
||||
});
|
||||
|
||||
it("compares the same filters in another order as equal", () => {
|
||||
expect(
|
||||
automationsFilterQueryString({ "ha-filter-labels": filter(["b", "a"]) })
|
||||
).toBe(
|
||||
automationsFilterQueryString({ "ha-filter-labels": filter(["a", "b"]) })
|
||||
);
|
||||
expect(
|
||||
automationsFilterQueryString({ "ha-filter-labels": filter([]) })
|
||||
).toBe(automationsFilterQueryString({}));
|
||||
});
|
||||
|
||||
it("keeps one category, as the category filter picks one", () => {
|
||||
expect(
|
||||
automationsLinkFromUrl("?category=a,b")?.filters["ha-filter-categories"]
|
||||
).toEqual(filter(["a"]));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user