mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-21 21:23:32 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbff548dba | ||
|
|
9f42e4ccc5 | ||
|
|
e0050fbb53 | ||
|
|
285f531809 | ||
|
|
0b29f30e2f | ||
|
|
78e9769743 | ||
|
|
9a89b4945b | ||
|
|
b54aa395e1 | ||
|
|
aa3c6b76b9 | ||
|
|
b9fdfebcce |
No files matched your search
@@ -71,6 +71,9 @@ export class QueryParamsController<
|
||||
mainWindow.removeEventListener("location-changed", this._navigated);
|
||||
mainWindow.removeEventListener("popstate", this._navigated);
|
||||
this._cancelPending();
|
||||
// A detached host can still get late events, like a filter that finished
|
||||
// a lookup, and must not write over the page that is open now
|
||||
this._path = undefined;
|
||||
}
|
||||
|
||||
/** The current search params, or undefined when not on the host's page. */
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import type {
|
||||
DataTableFiltersValue,
|
||||
DataTableFiltersValues,
|
||||
} 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 entities table. Search, sorting and grouping are kept
|
||||
// by the data table, see data-table-query-params.
|
||||
export const entitiesFilterQueryParamConfig = {
|
||||
list: [
|
||||
"status",
|
||||
"floor",
|
||||
"area",
|
||||
"device",
|
||||
"domain",
|
||||
"entity_domain",
|
||||
"label",
|
||||
"assistants",
|
||||
"config_entry",
|
||||
"sub_entry",
|
||||
// Read until 2027.5, replaced with `assistants` on the next write
|
||||
"voice_assistant",
|
||||
],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
export type EntitiesFilterQueryParams = QueryParamValues<
|
||||
typeof entitiesFilterQueryParamConfig
|
||||
>;
|
||||
|
||||
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 entitiesFilterQueryParams = (
|
||||
filters: DataTableFiltersValues
|
||||
): EntitiesFilterQueryParams => ({
|
||||
status: list(filters["ha-filter-states"]),
|
||||
floor: floorAreas(filters["ha-filter-floor-areas"])?.floors,
|
||||
area: floorAreas(filters["ha-filter-floor-areas"])?.areas,
|
||||
device: list(filters["ha-filter-devices"]),
|
||||
domain: list(filters["ha-filter-integrations"]),
|
||||
entity_domain: list(filters["ha-filter-domains"]),
|
||||
label: list(filters["ha-filter-labels"]),
|
||||
assistants: list(filters["ha-filter-voice-assistants"]),
|
||||
config_entry: list(filters.config_entry),
|
||||
sub_entry: list(filters.sub_entry),
|
||||
});
|
||||
|
||||
// The filters as the URL holds them, so values that filter the same, such as
|
||||
// no list and an empty one, compare equal
|
||||
export const entitiesFilterQueryString = (
|
||||
filters: DataTableFiltersValues
|
||||
): string =>
|
||||
createQueryString(
|
||||
entitiesFilterQueryParams(filters),
|
||||
entitiesFilterQueryParamConfig
|
||||
);
|
||||
|
||||
// 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 EntitiesLink {
|
||||
search: string;
|
||||
filters: DataTableFiltersValues;
|
||||
}
|
||||
|
||||
export const entitiesLinkFromUrl = (
|
||||
searchParams: SearchParamsSource
|
||||
): EntitiesLink | undefined => {
|
||||
if (
|
||||
!hasQueryParams(searchParams, entitiesFilterQueryParamConfig) &&
|
||||
!hasQueryParams(searchParams, dataTableQueryParamConfig)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = decodeQueryParams(
|
||||
searchParams,
|
||||
entitiesFilterQueryParamConfig
|
||||
);
|
||||
|
||||
const { search } = decodeQueryParams(searchParams, dataTableQueryParamConfig);
|
||||
|
||||
const values = (key: keyof EntitiesFilterQueryParams): string[] =>
|
||||
params[key]?.filter(Boolean) ?? [];
|
||||
|
||||
const floors = values("floor");
|
||||
const areas = values("area");
|
||||
const assistants = values("assistants");
|
||||
|
||||
return {
|
||||
search: search ?? "",
|
||||
filters: {
|
||||
"ha-filter-states": values("status"),
|
||||
"ha-filter-floor-areas":
|
||||
floors.length || areas.length ? { floors, areas } : undefined,
|
||||
"ha-filter-devices": values("device"),
|
||||
"ha-filter-integrations": values("domain"),
|
||||
"ha-filter-domains": values("entity_domain"),
|
||||
"ha-filter-labels": values("label"),
|
||||
"ha-filter-voice-assistants": assistants.length
|
||||
? assistants
|
||||
: values("voice_assistant"),
|
||||
config_entry: values("config_entry"),
|
||||
sub_entry: values("sub_entry"),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -3,13 +3,13 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DataTableSettingsController } from "../../../common/controllers/data-table-settings-controller";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-help-tooltip";
|
||||
@@ -52,10 +52,12 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
private _tableSettings = new DataTableSettingsController(this, {
|
||||
search: "application-credentials-table-search",
|
||||
sorting: "application-credentials-table-sort",
|
||||
});
|
||||
@storage({
|
||||
key: "application-credentials-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({
|
||||
key: "application-credentials-table-column-order",
|
||||
@@ -71,6 +73,15 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "application-credentials-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<ApplicationCredential> = {
|
||||
@@ -96,7 +107,6 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
urlName: "integration",
|
||||
},
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
@@ -165,14 +175,13 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
.initialSorting=${this._tableSettings.sorting}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@sorting-changed=${this._tableSettings.sortingChanged}
|
||||
.filter=${this._tableSettings.search}
|
||||
@search-changed=${this._tableSettings.searchChanged}
|
||||
sync-url
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
>
|
||||
<div class="header-btns" slot="selection-bar">
|
||||
${
|
||||
@@ -314,11 +323,19 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.table-header {
|
||||
display: flex;
|
||||
|
||||
@@ -38,6 +38,7 @@ export function getDomainTableColumn<T>(
|
||||
title: localize("ui.panel.config.generic.headers.domain"),
|
||||
hidden: true,
|
||||
groupable: true,
|
||||
urlName: "entity_domain",
|
||||
filterable: true,
|
||||
sortable: false,
|
||||
};
|
||||
@@ -49,6 +50,7 @@ export function getAreaTableColumn<T>(
|
||||
return {
|
||||
title: localize("ui.panel.config.generic.headers.area"),
|
||||
groupable: true,
|
||||
urlName: "area",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
minWidth: "120px",
|
||||
@@ -62,6 +64,7 @@ export function getFloorTableColumn<T>(
|
||||
title: localize("ui.panel.config.generic.headers.floor"),
|
||||
defaultHidden: true,
|
||||
groupable: true,
|
||||
urlName: "floor",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
minWidth: "120px",
|
||||
@@ -75,6 +78,7 @@ export function getCategoryTableColumn<T>(
|
||||
title: localize("ui.panel.config.generic.headers.category"),
|
||||
defaultHidden: true,
|
||||
groupable: true,
|
||||
urlName: "category",
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,8 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoize 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 { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
@@ -37,9 +39,14 @@ import {
|
||||
PROTOCOL_INTEGRATIONS,
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { getHistoryState, updateHistoryState } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
entitiesFilterQueryParamConfig,
|
||||
entitiesFilterQueryParams,
|
||||
entitiesFilterQueryString,
|
||||
entitiesLinkFromUrl,
|
||||
} from "../../../common/url/entities-query-params";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
@@ -48,7 +55,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-alert";
|
||||
@@ -154,6 +160,16 @@ export interface EntityRow extends StateEntity {
|
||||
available: string;
|
||||
}
|
||||
|
||||
// Local variant of data/expose/getExposedEntityVoiceAssistantIds, which works
|
||||
// for entities both with and without a unique ID
|
||||
const getVoiceAssistantIds = (
|
||||
exposedEntities: Record<string, ExposeEntitySettings> | undefined,
|
||||
entityId: string
|
||||
) =>
|
||||
Object.keys(voiceAssistants).filter(
|
||||
(vaId) => exposedEntities?.[entityId]?.[vaId]
|
||||
);
|
||||
|
||||
@customElement("ha-config-entities")
|
||||
export class HaConfigEntities extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -183,16 +199,15 @@ export class HaConfigEntities extends LitElement {
|
||||
// including entities without unique id
|
||||
@state() private _exposedEntities?: Record<string, ExposeEntitySettings>;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "entities-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter: string = getHistoryState()?.filter || "";
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
private _tableSettings = new DataTableSettingsController(
|
||||
this,
|
||||
{
|
||||
search: "entities-table-search",
|
||||
sorting: "entities-table-sort",
|
||||
grouping: "entities-table-grouping",
|
||||
},
|
||||
{ defaultGrouping: "device_full" }
|
||||
);
|
||||
|
||||
@state()
|
||||
private _filters: DataTableFiltersValues = {};
|
||||
@@ -217,12 +232,6 @@ export class HaConfigEntities extends LitElement {
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@storage({ key: "entities-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "entities-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "entities-table-collapsed",
|
||||
state: false,
|
||||
@@ -251,12 +260,19 @@ export class HaConfigEntities extends LitElement {
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
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,
|
||||
entitiesFilterQueryParamConfig,
|
||||
{
|
||||
enabled: () => true,
|
||||
navigated: () => this._applyLink(),
|
||||
}
|
||||
);
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("location-changed", this._locationChanged);
|
||||
window.addEventListener("popstate", this._popState);
|
||||
window.addEventListener(
|
||||
"exposed-entities-changed",
|
||||
this._fetchExposedEntities
|
||||
@@ -265,28 +281,12 @@ export class HaConfigEntities extends LitElement {
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("location-changed", this._locationChanged);
|
||||
window.removeEventListener("popstate", this._popState);
|
||||
window.removeEventListener(
|
||||
"exposed-entities-changed",
|
||||
this._fetchExposedEntities
|
||||
);
|
||||
}
|
||||
|
||||
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 _states = memoize((localize: LocalizeFunc) => [
|
||||
{
|
||||
value: "available",
|
||||
@@ -368,6 +368,7 @@ export class HaConfigEntities extends LitElement {
|
||||
title: localize("ui.panel.config.entities.picker.headers.device"),
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
urlName: "device",
|
||||
hidden: true,
|
||||
},
|
||||
area: getAreaTableColumn(localize),
|
||||
@@ -376,6 +377,7 @@ export class HaConfigEntities extends LitElement {
|
||||
title: localize("ui.panel.config.entities.picker.headers.integration"),
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
urlName: "integration",
|
||||
filterable: true,
|
||||
},
|
||||
domain: getDomainTableColumn(localize),
|
||||
@@ -462,18 +464,21 @@ export class HaConfigEntities extends LitElement {
|
||||
title: localize("ui.panel.config.entities.picker.headers.availability"),
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
urlName: "available",
|
||||
hidden: true,
|
||||
},
|
||||
visible: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.visibility"),
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
urlName: "visible",
|
||||
hidden: true,
|
||||
},
|
||||
enabled: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.enabled"),
|
||||
sortable: true,
|
||||
groupable: true,
|
||||
urlName: "enabled",
|
||||
hidden: true,
|
||||
},
|
||||
labels: getLabelsTableColumn(),
|
||||
@@ -495,6 +500,7 @@ export class HaConfigEntities extends LitElement {
|
||||
entitiesWithoutUniqueId: StateEntity[],
|
||||
filters: DataTableFiltersValues,
|
||||
filteredItems: DataTableFiltersItems,
|
||||
exposedEntities?: Record<string, ExposeEntitySettings>,
|
||||
entries?: ConfigEntry[],
|
||||
labelReg?: LabelRegistryEntry[]
|
||||
) => {
|
||||
@@ -554,19 +560,21 @@ export class HaConfigEntities extends LitElement {
|
||||
Array.isArray(filter) &&
|
||||
filter.length
|
||||
) {
|
||||
if (!(
|
||||
Array.isArray(this._filters.config_entry) &&
|
||||
this._filters.config_entry.length === 1
|
||||
)) {
|
||||
const configEntryIds = this._filters.config_entry;
|
||||
|
||||
if (!Array.isArray(configEntryIds) || !configEntryIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
filteredEntities = filteredEntities.filter(
|
||||
(entity) =>
|
||||
entity.config_subentry_id &&
|
||||
(filter as string[]).includes(entity.config_subentry_id)
|
||||
);
|
||||
if (!this._subEntries) {
|
||||
this._loadSubEntries(this._filters.config_entry[0]);
|
||||
|
||||
// The titles shown in the filter chip need a single config entry
|
||||
if (configEntryIds.length === 1 && !this._subEntries) {
|
||||
this._loadSubEntries(configEntryIds[0]);
|
||||
}
|
||||
} else if (
|
||||
key === "ha-filter-integrations" &&
|
||||
@@ -632,8 +640,8 @@ export class HaConfigEntities extends LitElement {
|
||||
filter.length
|
||||
) {
|
||||
filteredEntities = filteredEntities.filter((entity) =>
|
||||
this._getExposedEntityVoiceAssistantIds(entity.entity_id).some(
|
||||
(va) => (filter as string[]).includes(va)
|
||||
getVoiceAssistantIds(exposedEntities, entity.entity_id).some((va) =>
|
||||
(filter as string[]).includes(va)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -689,7 +697,9 @@ export class HaConfigEntities extends LitElement {
|
||||
? `${deviceName} (${areaName})`
|
||||
: deviceName
|
||||
: undefined;
|
||||
const assistants = this._getExposedEntityVoiceAssistantIds(
|
||||
|
||||
const assistants = getVoiceAssistantIds(
|
||||
exposedEntities,
|
||||
entry.entity_id
|
||||
);
|
||||
|
||||
@@ -793,6 +803,7 @@ export class HaConfigEntities extends LitElement {
|
||||
this._entitiesWithoutUniqueId,
|
||||
this._filters,
|
||||
this._filteredItems,
|
||||
this._exposedEntities,
|
||||
this._entries,
|
||||
this._labels
|
||||
);
|
||||
@@ -829,20 +840,21 @@ export class HaConfigEntities extends LitElement {
|
||||
}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
.initialGroupColumn=${this._activeGrouping ?? "device_full"}
|
||||
.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}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
clickable
|
||||
sync-url
|
||||
@clear-filter=${this._clearFilter}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
.filter=${this._tableSettings.search}
|
||||
@search-changed=${this._tableSettings.searchChanged}
|
||||
@row-click=${this._openEditEntry}
|
||||
id="entity_id"
|
||||
.hasFab=${includeAddDeviceFab}
|
||||
@@ -1069,62 +1081,69 @@ export class HaConfigEntities extends LitElement {
|
||||
|
||||
private _filterChanged(ev) {
|
||||
const type = ev.target.localName;
|
||||
const filters = { ...this._filters, [type]: ev.detail.value };
|
||||
|
||||
this._filters = { ...this._filters, [type]: ev.detail.value };
|
||||
this._filteredItems = { ...this._filteredItems, [type]: ev.detail.items };
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = this._filters;
|
||||
// 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 (
|
||||
entitiesFilterQueryString(filters) !==
|
||||
entitiesFilterQueryString(this._filters)
|
||||
) {
|
||||
this._storageFilters = {
|
||||
...this._storedFilters(),
|
||||
[type]: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
this._filters = filters;
|
||||
this._filteredItems = { ...this._filteredItems, [type]: ev.detail.items };
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._setFiltersFromUrl();
|
||||
this._fetchExposedEntities();
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
if (Object.keys(this._filters).length) {
|
||||
return;
|
||||
}
|
||||
this._filters = {
|
||||
"ha-filter-states": ["enabled"],
|
||||
};
|
||||
}
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
const area = this._searchParms.get("area");
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
const subEntry = this._searchParms.get("sub_entry");
|
||||
const device = this._searchParms.get("device");
|
||||
const label = this._searchParms.get("label");
|
||||
const voiceAssistant = this._searchParms.get("voice_assistant");
|
||||
// Links, like the ones on a device or integration 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 ? entitiesLinkFromUrl(searchParams) : undefined;
|
||||
|
||||
// Shown, but not stored as the user's choice
|
||||
if (link) {
|
||||
this._tableSettings.showSearch(link.search);
|
||||
this._filters = link.filters;
|
||||
} 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._storedFilters();
|
||||
queueMicrotask(() => this._writeFilters());
|
||||
|
||||
if (!area && !domain && !configEntry && !label && !device) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._fromUrl = true;
|
||||
this._filter = getHistoryState()?.filter || "";
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
this._filters = {
|
||||
"ha-filter-states": [],
|
||||
"ha-filter-floor-areas": area ? { areas: [area] } : undefined,
|
||||
"ha-filter-integrations": domain ? [domain] : [],
|
||||
"ha-filter-devices": device ? [device] : [],
|
||||
"ha-filter-labels": label ? [label] : [],
|
||||
"ha-filter-voice-assistants": voiceAssistant ? [voiceAssistant] : [],
|
||||
config_entry: configEntry ? [configEntry] : [],
|
||||
sub_entry: subEntry ? [subEntry] : [],
|
||||
};
|
||||
private _storedFilters(): DataTableFiltersValues {
|
||||
return Object.keys(this._storageFilters).length
|
||||
? this._storageFilters
|
||||
: { "ha-filter-states": ["enabled"] };
|
||||
}
|
||||
|
||||
private _writeFilters() {
|
||||
this._filterParams.write(entitiesFilterQueryParams(this._filters));
|
||||
}
|
||||
|
||||
private _clearFilter() {
|
||||
this._filters = {};
|
||||
this._filteredItems = {};
|
||||
if (!this._fromUrl) {
|
||||
this._storageFilters = {};
|
||||
}
|
||||
this._storageFilters = {};
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _fetchExposedEntities = async () => {
|
||||
@@ -1139,14 +1158,6 @@ export class HaConfigEntities extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
// local variant of data/expose/getExposedEntityVoiceAssistantIds
|
||||
// which works for both entities with and without unique id
|
||||
private _getExposedEntityVoiceAssistantIds(entityId: string) {
|
||||
return Object.keys(voiceAssistants).filter(
|
||||
(vaId) => this._exposedEntities?.[entityId]?.[vaId]
|
||||
);
|
||||
}
|
||||
|
||||
private _getExposedEntitySettingsAsOptions(
|
||||
entityId: string
|
||||
): EntityRegistryOptions {
|
||||
@@ -1165,8 +1176,8 @@ export class HaConfigEntities extends LitElement {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._filters = this._storageFilters;
|
||||
this._setFiltersFromUrl();
|
||||
this._filters = this._storedFilters();
|
||||
this._applyLink();
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass");
|
||||
@@ -1238,11 +1249,6 @@ export class HaConfigEntities extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
updateHistoryState({ filter: this._filter });
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
ev: HASSDomEvent<SelectionChangedEvent>
|
||||
): void {
|
||||
@@ -1541,6 +1547,7 @@ ${rejected
|
||||
this._entitiesWithoutUniqueId,
|
||||
this._filters,
|
||||
this._filteredItems,
|
||||
this._exposedEntities,
|
||||
this._entries,
|
||||
this._labels
|
||||
);
|
||||
@@ -1555,20 +1562,17 @@ ${rejected
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const integrations = this._filters["ha-filter-integrations"];
|
||||
showAddIntegrationDialog(this, {
|
||||
domain: this._searchParms.get("domain") || undefined,
|
||||
domain:
|
||||
Array.isArray(integrations) && integrations.length === 1
|
||||
? integrations[0]
|
||||
: undefined,
|
||||
navigateToResult: true,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ class HaConfigEntryDeviceRow extends LitElement {
|
||||
};
|
||||
|
||||
private _handleNavigateToEntities = () => {
|
||||
navigate(`/config/entities/?historyBack=1&device=${this.device.id}`);
|
||||
navigate(`/config/entities?historyBack=1&device=${this.device.id}`);
|
||||
};
|
||||
|
||||
private _doDisableDevice = async () => {
|
||||
|
||||
@@ -156,7 +156,7 @@ export class HaConfigEntryRow extends LitElement {
|
||||
} else if (!allDevices.length && !allServices.length && entities.length) {
|
||||
statusLine.push(
|
||||
html`<a
|
||||
href=${`/config/entities/?historyBack=1&config_entry=${item.entry_id}`}
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.entities",
|
||||
{ count: entities.length }
|
||||
|
||||
@@ -190,7 +190,7 @@ export class MatterConfigDashboard extends LitElement {
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
href=${`/config/entities?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
@@ -232,7 +232,7 @@ export class MQTTConfigPanel extends LitElement {
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
href=${`/config/entities?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
@@ -249,7 +249,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
href=${`/config/entities?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
+1
-1
@@ -338,7 +338,7 @@ class ZWaveJSConfigDashboard extends LitElement {
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
|
||||
href=${`/config/entities?historyBack=1&config_entry=${this.configEntryId}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
||||
@@ -54,17 +54,27 @@ const helpersTableParams = {
|
||||
assistants: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
// The application credentials table reads these from the link
|
||||
const applicationCredentialsTableParams = {
|
||||
// The entities table reads these from the link
|
||||
const entitiesTableParams = {
|
||||
search: "string?",
|
||||
sort: "string?",
|
||||
direction: "string?",
|
||||
group: "string?",
|
||||
status: "string?",
|
||||
floor: "string?",
|
||||
area: "string?",
|
||||
device: "string?",
|
||||
domain: "string?",
|
||||
entity_domain: "string?",
|
||||
label: "string?",
|
||||
assistants: "string?",
|
||||
config_entry: "string?",
|
||||
sub_entry: "string?",
|
||||
} satisfies Redirect["params"];
|
||||
|
||||
export const getMyRedirects = (): Redirects => ({
|
||||
application_credentials: {
|
||||
redirect: "/config/application_credentials",
|
||||
params: applicationCredentialsTableParams,
|
||||
},
|
||||
tools_assist: {
|
||||
redirect: "/config/tools/assist",
|
||||
@@ -254,6 +264,7 @@ export const getMyRedirects = (): Redirects => ({
|
||||
},
|
||||
entities: {
|
||||
redirect: "/config/entities",
|
||||
params: entitiesTableParams,
|
||||
},
|
||||
labels: {
|
||||
redirect: "/config/labels",
|
||||
|
||||
@@ -107,6 +107,17 @@ describe("QueryParamsController", () => {
|
||||
expect(navigated).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing once disconnected, even on the same page", () => {
|
||||
controller.hostDisconnected();
|
||||
controller.write({ search: "abc" });
|
||||
expect(currentUrl()).toBe("/page?other=1");
|
||||
expect(controller.searchParams()).toBeUndefined();
|
||||
|
||||
controller.hostConnected();
|
||||
controller.write({ search: "abc" });
|
||||
expect(currentUrl()).toBe("/page?other=1&search=abc");
|
||||
});
|
||||
|
||||
it("does nothing when not enabled", () => {
|
||||
enabled = false;
|
||||
controller.write({ search: "abc" });
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
entitiesFilterQueryParamConfig,
|
||||
entitiesFilterQueryParams,
|
||||
entitiesLinkFromUrl,
|
||||
} from "../../../src/common/url/entities-query-params";
|
||||
import {
|
||||
createQueryString,
|
||||
updateQueryString,
|
||||
} from "../../../src/common/url/query-params";
|
||||
|
||||
describe("entities query params", () => {
|
||||
it("reads nothing without table params", () => {
|
||||
expect(entitiesLinkFromUrl("?historyBack=1")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("replaces every filter and the search with what a link asks for", () => {
|
||||
expect(entitiesLinkFromUrl("?device=abc&label=a,b")).toEqual({
|
||||
search: "",
|
||||
filters: {
|
||||
"ha-filter-states": [],
|
||||
"ha-filter-floor-areas": undefined,
|
||||
"ha-filter-devices": ["abc"],
|
||||
"ha-filter-integrations": [],
|
||||
"ha-filter-domains": [],
|
||||
"ha-filter-labels": ["a", "b"],
|
||||
"ha-filter-voice-assistants": [],
|
||||
config_entry: [],
|
||||
sub_entry: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("still reads voice_assistant, with assistants winning", () => {
|
||||
expect(
|
||||
entitiesLinkFromUrl("?voice_assistant=conversation")?.filters[
|
||||
"ha-filter-voice-assistants"
|
||||
]
|
||||
).toEqual(["conversation"]);
|
||||
expect(
|
||||
entitiesLinkFromUrl(
|
||||
"?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 = entitiesLinkFromUrl(url);
|
||||
|
||||
expect(
|
||||
updateQueryString(
|
||||
url,
|
||||
entitiesFilterQueryParams(link?.filters ?? {}),
|
||||
entitiesFilterQueryParamConfig
|
||||
)
|
||||
).toBe("historyBack=1&assistants=conversation");
|
||||
});
|
||||
|
||||
it("round trips the filters", () => {
|
||||
const filters = {
|
||||
"ha-filter-states": ["enabled", "hidden"],
|
||||
"ha-filter-floor-areas": { floors: ["ground"], areas: ["kitchen"] },
|
||||
"ha-filter-devices": ["abc"],
|
||||
"ha-filter-integrations": ["hue"],
|
||||
"ha-filter-domains": ["light"],
|
||||
"ha-filter-labels": ["a"],
|
||||
"ha-filter-voice-assistants": ["conversation"],
|
||||
config_entry: ["entry"],
|
||||
sub_entry: ["sub"],
|
||||
};
|
||||
|
||||
expect(
|
||||
entitiesLinkFromUrl(
|
||||
createQueryString(
|
||||
entitiesFilterQueryParams(filters),
|
||||
entitiesFilterQueryParamConfig
|
||||
)
|
||||
)
|
||||
).toEqual({ search: "", filters });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user