mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-21 21:23:32 +00:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62c8534e43 | ||
|
|
526b39e779 | ||
|
|
9d9841da0b | ||
|
|
48c870cf40 | ||
|
|
664b74479d | ||
|
|
04240a18ba | ||
|
|
22f26b0ed7 | ||
|
|
c6ca2e0dd0 |
No files matched your search
@@ -46,7 +46,7 @@ When a router reuses one element for different items, such as one app's tab afte
|
||||
- Debounce search writes. Write sort, group and filter changes straight away.
|
||||
- Leave empty and default values out of the URL, such as an empty search or ascending direction.
|
||||
- A param in the URL wins over stored preferences. A link with none of the page's params keeps what the user picked before; `hasQueryParams` makes that check.
|
||||
- A link with any of the page's params replaces the search and every filter, since a filter left over from before would hide part of what the link shows. Store what the link applied as the user's choice.
|
||||
- A link with any of the page's params replaces the search and every filter, since a filter left over from before would hide part of what the link shows. Show what the link applied, but don't store it: links often carry someone else's choices, such as a My Home Assistant link made with `m`. Store only what the user changes on the page, and only that setting.
|
||||
- The URL is the whole state. Don't add filters while applying a link; a link that needs one, such as showing disabled devices, says so in its params (`status=disabled`).
|
||||
- A link to a page that is already open must apply the new URL. Handle `navigated()`, not only the first render. The router often keeps a page alive, so this is the common case, not an edge case.
|
||||
- Use one comma-separated param for several values (`?status=new,installed`), which `list` params do.
|
||||
@@ -58,12 +58,13 @@ When a router reuses one element for different items, such as one app's tab afte
|
||||
|
||||
## Data Tables
|
||||
|
||||
`hass-tabs-subpage-data-table` with the `sync-url` attribute keeps `search`, `sort`, `direction` and `group` in the URL through `dataTableQueryParamsFromState` and `dataTableQueryStateFromUrl`. A linked search replaces the current one, and sorting or grouping a link leaves out keeps what was picked before. The table fires `search-changed`, `sorting-changed` and `grouping-changed` when a link changes them, so the page can keep its stored values in step.
|
||||
`hass-tabs-subpage-data-table` with the `sync-url` attribute keeps `search`, `sort`, `direction` and `group` in the URL through `dataTableQueryParamsFromState` and `dataTableQueryStateFromUrl`. A linked search replaces the current one, and sorting or grouping a link leaves out keeps what was picked before. The table fires `search-changed`, `sorting-changed` and `grouping-changed` when a link changes them, with `link` set, so the page shows them without storing them. Use `DataTableSettingsController` (`src/common/controllers/data-table-settings-controller.ts`) for the stored search, sorting and grouping: give it the page's storage keys, bind its `search`, `sorting` and `grouping` to the table, and its `searchChanged`, `sortingChanged` and `groupingChanged` to the table's events. Call `showSearch` for a search the page reads from a link itself. A page the router keeps open shows what the user stored when it's opened again without params, like from the sidebar: the controller and table restore the search, sorting and grouping, and a page with filters restores its stored filters when its URL has no params.
|
||||
|
||||
- Give sortable and groupable columns a short public `urlName` (for example `type`, not `translated_category`). Sorting falls back to the column id without one; grouping is only written for columns that have one, so internal ids don't end up in links.
|
||||
- When a column definition is shared between pages, set its `urlName` where the shared column is defined, so every page uses the same name.
|
||||
- The table reads the URL once, as soon as it has columns. When a column only exists once data has loaded, such as Filename on the dashboards page, turn syncing on only after loading succeeds (`?sync-url=${!this._loading && !this._loadFailed}`), or a link that sorts or groups by that column is dropped.
|
||||
- The page owns its filters. Give it a filter config in its `*-query-params.ts`, a helper that turns filter values into params, and one that turns a link back into the search and filter values. The link helper returns `undefined` when the URL has none of the filter params and none of the table's (`dataTableQueryParamConfig`), so stored choices stay. Use a second controller on the page for them, as `ha-marketplace-dashboard.ts` and `lorawan-devices.ts` do. `browseUrl` and `browseLinkFromUrl` in `src/panels/marketplace/dashboards/dashboard-repositories.ts` show the pair.
|
||||
- Filter components also send the value the page gave them once they have looked up their items, so a filter event isn't always the user's. Store a filter only when it changes what the URL holds, comparing the filters' query strings before and after, as `helpersFilterQueryString` does, and store only that filter.
|
||||
- A memoized filter that also reads async state (anything loaded after the page connects) must take that state as a memoize argument, or a linked filter shows stale results until something else changes.
|
||||
- A filter component that looks up related items asynchronously should drop a lookup whose value has since changed, or two links landing within one lookup apply the older one.
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import type {
|
||||
ReactiveController,
|
||||
ReactiveControllerHost,
|
||||
} from "@lit/reactive-element/reactive-controller";
|
||||
import type { SortingChangedEvent } from "../../components/data-table/ha-data-table";
|
||||
import { getStorage } from "../decorators/storage";
|
||||
import type { HASSDomEvent } from "../dom/fire_event";
|
||||
import type { QueryParamConfig } from "../url/query-params";
|
||||
import { QueryParamsController } from "./query-params-controller";
|
||||
|
||||
export interface DataTableSettingsKeys {
|
||||
/** Session storage key for the search. */
|
||||
search?: string;
|
||||
/** Local storage key for the sorting. */
|
||||
sorting?: string;
|
||||
/** Local storage key for the grouping. */
|
||||
grouping?: string;
|
||||
}
|
||||
|
||||
export interface DataTableSettingsOptions {
|
||||
/** The grouping when none is stored. */
|
||||
defaultGrouping?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the search, sorting and grouping a user picks on a data table page.
|
||||
* What a link applies (`link` on the table's events) is shown, but not
|
||||
* stored, as links often carry someone else's choices. Bind
|
||||
* `searchChanged`, `sortingChanged` and `groupingChanged` to the table's
|
||||
* events.
|
||||
*/
|
||||
export class DataTableSettingsController implements ReactiveController {
|
||||
/** The search shown, which can be a link's. */
|
||||
public search: string;
|
||||
|
||||
/** The grouping shown, which can be a link's. */
|
||||
public grouping?: string;
|
||||
|
||||
// The search is kept for the session, sorting and grouping for good
|
||||
private _sessionStorage = getStorage("sessionStorage");
|
||||
|
||||
private _localStorage = getStorage("localStorage");
|
||||
|
||||
// Only used to know when the page is opened again
|
||||
private _queryParams: QueryParamsController<QueryParamConfig>;
|
||||
|
||||
constructor(
|
||||
private _host: ReactiveControllerHost,
|
||||
private _keys: DataTableSettingsKeys,
|
||||
private _options: DataTableSettingsOptions = {}
|
||||
) {
|
||||
this._host.addController(this);
|
||||
this._queryParams = new QueryParamsController(
|
||||
this._host,
|
||||
{},
|
||||
{
|
||||
enabled: () => true,
|
||||
navigated: () => this._navigated(),
|
||||
}
|
||||
);
|
||||
this.search = this._storedSearch();
|
||||
this.grouping = this._storedGrouping();
|
||||
}
|
||||
|
||||
/** The stored sorting, for the table to start with. */
|
||||
public get sorting(): SortingChangedEvent | undefined {
|
||||
return this._keys.sorting
|
||||
? this._localStorage.getValue<SortingChangedEvent>(this._keys.sorting)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
/** Show a search, like one a page reads from a link, without storing it. */
|
||||
public showSearch(value: string) {
|
||||
this.search = value;
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
public searchChanged = (
|
||||
ev: HASSDomEvent<HASSDomEvents["search-changed"]>
|
||||
) => {
|
||||
this.showSearch(ev.detail.value);
|
||||
|
||||
if (this._keys.search && !ev.detail.link) {
|
||||
this._sessionStorage.setValue(this._keys.search, ev.detail.value);
|
||||
}
|
||||
};
|
||||
|
||||
public sortingChanged = (ev: HASSDomEvent<SortingChangedEvent>) => {
|
||||
if (this._keys.sorting && !ev.detail.link) {
|
||||
const { column, direction } = ev.detail;
|
||||
this._localStorage.setValue(this._keys.sorting, { column, direction });
|
||||
}
|
||||
};
|
||||
|
||||
public groupingChanged = (
|
||||
ev: HASSDomEvent<HASSDomEvents["grouping-changed"]>
|
||||
) => {
|
||||
this.grouping = ev.detail.value;
|
||||
this._host.requestUpdate();
|
||||
|
||||
if (this._keys.grouping && !ev.detail.link) {
|
||||
this._localStorage.setValue(this._keys.grouping, ev.detail.value);
|
||||
}
|
||||
};
|
||||
|
||||
// A page opened again without params, like from the sidebar, shows what
|
||||
// the user stored rather than what a link showed before
|
||||
private _navigated() {
|
||||
if (this._queryParams.searchParams()?.toString() !== "") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.search = this._storedSearch();
|
||||
this.grouping = this._storedGrouping();
|
||||
this._host.requestUpdate();
|
||||
}
|
||||
|
||||
private _storedSearch(): string {
|
||||
return (
|
||||
(this._keys.search
|
||||
? this._sessionStorage.getValue<string>(this._keys.search)
|
||||
: undefined) ?? ""
|
||||
);
|
||||
}
|
||||
|
||||
private _storedGrouping(): string | undefined {
|
||||
return (
|
||||
(this._keys.grouping
|
||||
? this._localStorage.getValue<string>(this._keys.grouping)
|
||||
: undefined) ?? this._options.defaultGrouping
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -76,11 +76,15 @@ class StorageClass {
|
||||
return storageKey in this._storage;
|
||||
}
|
||||
|
||||
public getValue(storageKey: string): any {
|
||||
public getValue<T = any>(storageKey: string): T | undefined {
|
||||
if (!(storageKey in this._storage)) {
|
||||
this.addFromStorage(storageKey);
|
||||
}
|
||||
|
||||
return this._storage[storageKey];
|
||||
}
|
||||
|
||||
public setValue(storageKey: string, value: any): any {
|
||||
public setValue<T = any>(storageKey: string, value: T): void {
|
||||
const oldValue = this._storage[storageKey];
|
||||
this._storage[storageKey] = value;
|
||||
try {
|
||||
@@ -103,9 +107,22 @@ class StorageClass {
|
||||
|
||||
const storages: Record<string, StorageClass> = {};
|
||||
|
||||
export type StorageName = "localStorage" | "sessionStorage";
|
||||
|
||||
// Shared with `@storage`, so code that isn't a decorated field sees the same values
|
||||
export const getStorage = (
|
||||
storageName: StorageName = "localStorage"
|
||||
): StorageClass => {
|
||||
if (!(storageName in storages)) {
|
||||
storages[storageName] = new StorageClass(window[storageName]);
|
||||
}
|
||||
|
||||
return storages[storageName];
|
||||
};
|
||||
|
||||
export function storage(options: {
|
||||
key?: string;
|
||||
storage?: "localStorage" | "sessionStorage";
|
||||
storage?: StorageName;
|
||||
subscribe?: boolean;
|
||||
state?: boolean;
|
||||
serializer?: (value: any) => any;
|
||||
@@ -119,15 +136,7 @@ export function storage(options: {
|
||||
throw new Error("This decorator does not support this compilation type.");
|
||||
}
|
||||
|
||||
const storageName = options.storage || "localStorage";
|
||||
|
||||
let storageInstance: StorageClass;
|
||||
if (storageName && storageName in storages) {
|
||||
storageInstance = storages[storageName];
|
||||
} else {
|
||||
storageInstance = new StorageClass(window[storageName]);
|
||||
storages[storageName] = storageInstance;
|
||||
}
|
||||
const storageInstance = getStorage(options.storage);
|
||||
|
||||
const storageKey = options.key || String(propertyKey);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
} from "../../data/data_table_filters";
|
||||
import { dataTableQueryParamConfig } from "./data-table-query-params";
|
||||
import {
|
||||
createQueryString,
|
||||
decodeQueryParams,
|
||||
hasQueryParams,
|
||||
type QueryParamConfig,
|
||||
@@ -49,6 +50,16 @@ export const helpersFilterQueryParams = (
|
||||
assistants: list(filters["ha-filter-voice-assistants"]),
|
||||
});
|
||||
|
||||
// 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 helpersFilterQueryString = (
|
||||
filters: DataTableFiltersValues
|
||||
): string =>
|
||||
createQueryString(
|
||||
helpersFilterQueryParams(filters),
|
||||
helpersFilterQueryParamConfig
|
||||
);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface CollapsedChangedEvent {
|
||||
export interface SortingChangedEvent {
|
||||
column: string;
|
||||
direction: SortingDirection;
|
||||
// Set when a link sorted the table, not the user
|
||||
link?: boolean;
|
||||
}
|
||||
|
||||
export type SortingDirection = "desc" | "asc" | null;
|
||||
|
||||
@@ -260,6 +260,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
private _typedSearch?: string;
|
||||
|
||||
private _restoreInitial = false;
|
||||
|
||||
private _queryParams = new QueryParamsController(
|
||||
this,
|
||||
dataTableQueryParamConfig,
|
||||
@@ -267,6 +269,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
enabled: () => this.syncUrl,
|
||||
navigated: () => {
|
||||
if (this._urlRead) {
|
||||
// Opened again without params, like from the sidebar
|
||||
this._restoreInitial =
|
||||
this._queryParams.searchParams()?.toString() === "";
|
||||
this._applyUrl();
|
||||
}
|
||||
},
|
||||
@@ -300,6 +305,13 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
// Done here, after the page has set the initial sorting and grouping it
|
||||
// stored
|
||||
if (this._restoreInitial) {
|
||||
this._restoreInitial = false;
|
||||
this._restoreInitialState();
|
||||
}
|
||||
|
||||
if (this.syncUrl && !this._urlRead && Object.keys(this.columns).length) {
|
||||
this._urlRead = true;
|
||||
this._applyUrl();
|
||||
@@ -708,8 +720,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
fireEvent(this, "clear-filter");
|
||||
}
|
||||
|
||||
// A link with table params wins over what was picked before, the page is
|
||||
// told so it keeps the same
|
||||
// A link with table params wins over what was picked before. The page is
|
||||
// told with `link` set, so it shows it without storing it as the user's
|
||||
// choice.
|
||||
private _applyUrl() {
|
||||
const searchParams = this._queryParams.searchParams();
|
||||
const linked = searchParams
|
||||
@@ -719,7 +732,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
if (linked) {
|
||||
if (linked.search !== this.filter) {
|
||||
this.filter = linked.search;
|
||||
fireEvent(this, "search-changed", { value: this.filter });
|
||||
fireEvent(this, "search-changed", { value: this.filter, link: true });
|
||||
}
|
||||
if (
|
||||
linked.sorting &&
|
||||
@@ -728,10 +741,10 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
) {
|
||||
this._sortColumn = linked.sorting.column;
|
||||
this._sortDirection = linked.sorting.direction;
|
||||
fireEvent(this, "sorting-changed", linked.sorting);
|
||||
fireEvent(this, "sorting-changed", { ...linked.sorting, link: true });
|
||||
}
|
||||
if (linked.groupColumn && linked.groupColumn !== this._groupColumn) {
|
||||
this._setGroupColumn(linked.groupColumn);
|
||||
this._setGroupColumn(linked.groupColumn, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,6 +752,36 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private _restoreInitialState() {
|
||||
// Without a stored sort, use the column's default like a fresh load does
|
||||
const defaultColumn = Object.keys(this.columns).find(
|
||||
(columnId) => this.columns[columnId].direction
|
||||
);
|
||||
|
||||
const defaultDirection = defaultColumn
|
||||
? this.columns[defaultColumn].direction
|
||||
: undefined;
|
||||
|
||||
const sorting =
|
||||
this.initialSorting && this.columns[this.initialSorting.column]
|
||||
? this.initialSorting
|
||||
: defaultColumn && defaultDirection
|
||||
? { column: defaultColumn, direction: defaultDirection }
|
||||
: undefined;
|
||||
|
||||
this._sortColumn = sorting?.column;
|
||||
this._sortDirection = sorting?.direction ?? null;
|
||||
|
||||
const groupColumn =
|
||||
this.initialGroupColumn && this.columns[this.initialGroupColumn]
|
||||
? this.initialGroupColumn
|
||||
: "";
|
||||
|
||||
if (groupColumn !== (this._groupColumn ?? "")) {
|
||||
this._setGroupColumn(groupColumn, true);
|
||||
}
|
||||
}
|
||||
|
||||
private _flushUrl() {
|
||||
this._queryParams.flush();
|
||||
}
|
||||
@@ -807,9 +850,9 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
this._setGroupColumn(group);
|
||||
}
|
||||
|
||||
private _setGroupColumn(columnId: string) {
|
||||
private _setGroupColumn(columnId: string, link = false) {
|
||||
this._groupColumn = columnId;
|
||||
fireEvent(this, "grouping-changed", { value: columnId });
|
||||
fireEvent(this, "grouping-changed", { value: columnId, link });
|
||||
}
|
||||
|
||||
private _openSettings() {
|
||||
@@ -1126,8 +1169,9 @@ declare global {
|
||||
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"search-changed": { value: string };
|
||||
"grouping-changed": { value: string };
|
||||
// `link` is set when a link changed it, not the user
|
||||
"search-changed": { value: string; link?: boolean };
|
||||
"grouping-changed": { value: string; link?: boolean };
|
||||
"columns-changed": {
|
||||
columnOrder: string[] | undefined;
|
||||
hiddenColumns: string[] | undefined;
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } 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 { QueryParamsController } from "../../../common/controllers/query-params-controller";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
@@ -35,6 +36,7 @@ import type {
|
||||
import {
|
||||
helpersFilterQueryParamConfig,
|
||||
helpersFilterQueryParams,
|
||||
helpersFilterQueryString,
|
||||
helpersLinkFromUrl,
|
||||
} from "../../../common/url/helpers-query-params";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
@@ -46,7 +48,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";
|
||||
@@ -193,11 +194,15 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||
|
||||
@storage({ key: "helpers-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "helpers-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
private _tableSettings = new DataTableSettingsController(
|
||||
this,
|
||||
{
|
||||
search: "helpers-table-search",
|
||||
sorting: "helpers-table-sort",
|
||||
grouping: "helpers-table-grouping",
|
||||
},
|
||||
{ defaultGrouping: "category" }
|
||||
);
|
||||
|
||||
@storage({
|
||||
key: "helpers-table-collapsed",
|
||||
@@ -206,15 +211,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "helpers-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@storage({
|
||||
key: "helpers-table-column-order",
|
||||
state: false,
|
||||
@@ -692,21 +688,21 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
.columns=${this._columns(this.hass.localize, helpers)}
|
||||
.data=${helpers}
|
||||
.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}
|
||||
.activeFilters=${this._activeFilters}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@row-click=${this._openEditDialog}
|
||||
sync-url
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
.filter=${this._tableSettings.search}
|
||||
@search-changed=${this._tableSettings.searchChanged}
|
||||
has-fab
|
||||
clickable
|
||||
.noDataText=${this.hass.localize(
|
||||
@@ -874,10 +870,22 @@ export class HaConfigHelpers extends SubscribeMixin(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 };
|
||||
// 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 (
|
||||
helpersFilterQueryString(filters) !==
|
||||
helpersFilterQueryString(this._filters)
|
||||
) {
|
||||
this._storageFilters = {
|
||||
...this._storageFilters,
|
||||
[type]: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
this._filters = filters;
|
||||
this._filteredItems = { ...this._filteredItems, [type]: ev.detail.items };
|
||||
this._storageFilters = this._filters;
|
||||
this._writeFilters();
|
||||
this._applyFilters();
|
||||
}
|
||||
@@ -1025,11 +1033,19 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
const searchParams = this._filterParams.searchParams();
|
||||
const link = searchParams ? helpersLinkFromUrl(searchParams) : undefined;
|
||||
|
||||
// Shown, but not stored as the user's choice
|
||||
if (link) {
|
||||
this._filter = link.search;
|
||||
this._tableSettings.showSearch(link.search);
|
||||
this._filters = link.filters;
|
||||
this._storageFilters = 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._storageFilters;
|
||||
this._applyFilters();
|
||||
queueMicrotask(() => this._writeFilters());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._writeFilters();
|
||||
@@ -1573,22 +1589,10 @@ ${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;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
|
||||
@@ -3,12 +3,12 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DataTableSettingsController } from "../../../common/controllers/data-table-settings-controller";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-icon";
|
||||
import "../../../components/ha-button";
|
||||
@@ -44,11 +44,11 @@ export class HaConfigUsers extends LitElement {
|
||||
|
||||
@state() private _loadFailed = false;
|
||||
|
||||
@storage({ key: "users-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "users-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
private _tableSettings = new DataTableSettingsController(this, {
|
||||
search: "users-table-search",
|
||||
sorting: "users-table-sort",
|
||||
grouping: "users-table-grouping",
|
||||
});
|
||||
|
||||
@storage({
|
||||
key: "users-table-column-order",
|
||||
@@ -64,14 +64,6 @@ export class HaConfigUsers extends LitElement {
|
||||
})
|
||||
private _activeHiddenColumns?: string[];
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "users-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@storage({
|
||||
key: "users-table-collapsed",
|
||||
state: false,
|
||||
@@ -195,14 +187,14 @@ export class HaConfigUsers extends LitElement {
|
||||
.columnOrder=${this._activeColumnOrder}
|
||||
.hiddenColumns=${this._activeHiddenColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialGroupColumn=${this._tableSettings.grouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
.initialSorting=${this._tableSettings.sorting}
|
||||
@sorting-changed=${this._tableSettings.sortingChanged}
|
||||
@grouping-changed=${this._tableSettings.groupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
.filter=${this._tableSettings.search}
|
||||
@search-changed=${this._tableSettings.searchChanged}
|
||||
sync-url
|
||||
@row-click=${this._editUser}
|
||||
has-fab
|
||||
@@ -308,22 +300,10 @@ export class HaConfigUsers extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleColumnsChanged(ev: CustomEvent) {
|
||||
this._activeColumnOrder = ev.detail.columnOrder;
|
||||
this._activeHiddenColumns = ev.detail.hiddenColumns;
|
||||
|
||||
@@ -25,15 +25,18 @@ import { consume } from "../../../common/decorators/consume";
|
||||
import { consumeEntityStates } from "../../../common/decorators/consume-context-entry";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { DataTableSettingsController } from "../../../common/controllers/data-table-settings-controller";
|
||||
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import { marketplaceFilterQueryParamConfig } from "../../../common/url/marketplace-query-params";
|
||||
import {
|
||||
createMarketplaceFilterQueryString,
|
||||
marketplaceFilterQueryParamConfig,
|
||||
} from "../../../common/url/marketplace-query-params";
|
||||
import { shallowEqual } from "../../../common/util/shallow-equal";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
SelectionChangedEvent,
|
||||
SortingDirection,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
@@ -201,27 +204,25 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
|
||||
@state() private _marketplaceUpdateEntityIds: string[] = [];
|
||||
|
||||
@state() private _filters: RepositoryFilters = {};
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "marketplace-dashboard-table-filters",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filters: RepositoryFilters = {};
|
||||
|
||||
@storage({
|
||||
key: "marketplace-dashboard-table-sorting",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: { column: string; direction: SortingDirection };
|
||||
private _storageFilters: RepositoryFilters = {};
|
||||
|
||||
@storage({
|
||||
key: "marketplace-dashboard-table-grouping",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeGrouping?: string = "translated_status";
|
||||
private _tableSettings = new DataTableSettingsController(
|
||||
this,
|
||||
{
|
||||
search: "marketplace-dashboard-table-search",
|
||||
sorting: "marketplace-dashboard-table-sorting",
|
||||
grouping: "marketplace-dashboard-table-grouping",
|
||||
},
|
||||
{ defaultGrouping: "translated_status" }
|
||||
);
|
||||
|
||||
@storage({
|
||||
key: "marketplace-dashboard-table-collapsed",
|
||||
@@ -230,14 +231,6 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
})
|
||||
private _activeCollapsed: string[] = [];
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "marketplace-dashboard-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSearch?: string;
|
||||
|
||||
@storage({
|
||||
key: "marketplace-dashboard-table-hidden-columns",
|
||||
state: true,
|
||||
@@ -281,6 +274,7 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
// Stored settings ignore what is set before the first update
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated) {
|
||||
this._filters = this._storageFilters;
|
||||
this._applyLink();
|
||||
}
|
||||
|
||||
@@ -299,8 +293,9 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
const searchParams = this._filterParams.searchParams();
|
||||
const link = searchParams ? browseLinkFromUrl(searchParams) : undefined;
|
||||
|
||||
// Shown, but not stored as the user's choice
|
||||
if (link) {
|
||||
this._activeSearch = link.search;
|
||||
this._tableSettings.showSearch(link.search);
|
||||
this._filters = {
|
||||
...link.filters,
|
||||
[STATUS_FILTER]: statusFilterOfTab(
|
||||
@@ -309,6 +304,13 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
link.filters[STATUS_FILTER]
|
||||
),
|
||||
};
|
||||
} 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._storageFilters;
|
||||
queueMicrotask(() => this._writeFilters());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._writeFilters();
|
||||
@@ -377,32 +379,32 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
back-path="/config"
|
||||
clickable
|
||||
sync-url
|
||||
.filter=${this._activeSearch || ""}
|
||||
.filter=${this._tableSettings.search}
|
||||
has-filters
|
||||
.filters=${
|
||||
Object.values(filters).filter((values) => values?.length).length
|
||||
}
|
||||
.noDataText=${this._i18n.localize("ui.panel.marketplace.dashboard.no_data")}
|
||||
.empty=${!this.marketplace.repositories.length}
|
||||
.initialSorting=${this._activeSorting}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialSorting=${this._tableSettings.sorting}
|
||||
.initialGroupColumn=${this._tableSettings.grouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.groupOrder=${this._groupOrder(
|
||||
this._activeGrouping,
|
||||
this._tableSettings.grouping,
|
||||
this._i18n.localize
|
||||
)}
|
||||
.columnOrder=${this._orderTableColumns}
|
||||
.hiddenColumns=${this._hiddenTableColumns}
|
||||
@columns-changed=${this._handleColumnsChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@grouping-changed=${this._tableSettings.groupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@row-click=${this._handleRowClicked}
|
||||
?selectable=${this.tab === "installed"}
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
@clear-filter=${this._handleClearFilter}
|
||||
@search-changed=${this._handleSearchFilterChanged}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@search-changed=${this._tableSettings.searchChanged}
|
||||
@sorting-changed=${this._tableSettings.sortingChanged}
|
||||
>
|
||||
${this._renderToolbar()}
|
||||
${
|
||||
@@ -1002,9 +1004,18 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
navigate(`/marketplace/repository/${ev.detail.id}`);
|
||||
}
|
||||
|
||||
// A filter can also send the value it was given. Only a change to what the
|
||||
// URL holds is the user's, so what a link applied isn't stored.
|
||||
private _isUserChange(filters: RepositoryFilters): boolean {
|
||||
return (
|
||||
createMarketplaceFilterQueryString(filtersOfTab(filters, this.tab)) !==
|
||||
createMarketplaceFilterQueryString(filtersOfTab(this._filters, this.tab))
|
||||
);
|
||||
}
|
||||
|
||||
// The pane's own clear button sends no value
|
||||
private _statusFilterChanged(ev: HASSDomEvent<{ value?: string[] }>) {
|
||||
this._filters = {
|
||||
const filters = {
|
||||
...this._filters,
|
||||
[STATUS_FILTER]: statusFilterOfTab(
|
||||
this._filters,
|
||||
@@ -1012,33 +1023,50 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
ev.detail.value
|
||||
),
|
||||
};
|
||||
|
||||
if (this._isUserChange(filters)) {
|
||||
this._storageFilters = {
|
||||
...this._storageFilters,
|
||||
[STATUS_FILTER]: statusFilterOfTab(
|
||||
this._storageFilters,
|
||||
this.tab,
|
||||
ev.detail.value
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
this._filters = filters;
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _typeFilterChanged(ev: CustomEvent<{ value: string[] }>) {
|
||||
this._filters = { ...this._filters, [TYPE_FILTER]: ev.detail.value };
|
||||
const filters = { ...this._filters, [TYPE_FILTER]: ev.detail.value };
|
||||
|
||||
if (this._isUserChange(filters)) {
|
||||
this._storageFilters = {
|
||||
...this._storageFilters,
|
||||
[TYPE_FILTER]: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
this._filters = filters;
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _sourceFilterChanged(ev: CustomEvent<{ value: string[] }>) {
|
||||
this._filters = { ...this._filters, [SOURCE_FILTER]: ev.detail.value };
|
||||
const filters = { ...this._filters, [SOURCE_FILTER]: ev.detail.value };
|
||||
|
||||
if (this._isUserChange(filters)) {
|
||||
this._storageFilters = {
|
||||
...this._storageFilters,
|
||||
[SOURCE_FILTER]: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
this._filters = filters;
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _handleSearchFilterChanged(ev: CustomEvent) {
|
||||
this._activeSearch = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["grouping-changed"]>
|
||||
) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["collapsed-changed"]>
|
||||
) {
|
||||
@@ -1052,6 +1080,7 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
|
||||
private _handleClearFilter() {
|
||||
this._filters = {};
|
||||
this._storageFilters = {};
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user