Compare commits

...
9 changed files with 354 additions and 141 deletions

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
);
}
}
+21 -12
View File
@@ -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);
+11
View File
@@ -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;
+53 -9
View File
@@ -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;
+41 -37
View File
@@ -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;
+12 -32
View File
@@ -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();
}