Compare commits

...
6 changed files with 222 additions and 15 deletions

No files matched your search

@@ -15,6 +15,10 @@ import {
const DEBOUNCE_WAIT = 300;
// A page can be opened with or without a trailing slash
const trimSlash = (path: string) =>
path.length > 1 && path.endsWith("/") ? path.slice(0, -1) : path;
export interface QueryParamsControllerOptions {
/** Whether the host keeps its state in the URL. */
enabled: () => boolean;
@@ -24,6 +28,12 @@ export interface QueryParamsControllerOptions {
* the URL with `searchParams()` and apply it, then `write()` the state back.
*/
navigated: () => void;
/**
* The path of the host's page, for a page that can first be connected on
* a sub-path it sends on to itself, like an add link. Defaults to the path
* the host is first connected on.
*/
path?: string;
}
/**
@@ -57,7 +67,7 @@ export class QueryParamsController<
public hostConnected() {
// A cached page is reconnected on the way to another page, before its
// router has moved on, so it keeps the page it was first connected on
this._path ??= currentPath();
this._path ??= this._options.path ?? currentPath();
this._connected = true;
mainWindow.addEventListener("location-changed", this._navigated);
mainWindow.addEventListener("popstate", this._navigated);
@@ -127,7 +137,9 @@ export class QueryParamsController<
private _isActive(): boolean {
return (
this._connected && this._options.enabled() && currentPath() === this._path
this._connected &&
this._options.enabled() &&
trimSlash(currentPath()) === trimSlash(this._path ?? "")
);
}
@@ -158,7 +170,9 @@ export class QueryParamsController<
const current = mainWindow.location.search;
const queryString = updateQueryString(current, values, this._config);
if (current !== (queryString ? `?${queryString}` : "")) {
replaceCurrentUrl(constructUrlCurrentPath(queryString));
// Keep a fragment like #config_entry=, the demo already routes with it
const hash = __DEMO__ ? "" : mainWindow.location.hash;
replaceCurrentUrl(`${constructUrlCurrentPath(queryString)}${hash}`);
}
}
}
@@ -0,0 +1,70 @@
import {
decodeQueryParams,
hasQueryParams,
type QueryParamConfig,
type QueryParamValues,
type SearchParamsSource,
} from "./query-params";
// `status` adds the ignored and disabled integrations to what is shown.
export const integrationsQueryParamConfig = {
string: ["search"],
list: ["status"],
} as const satisfies QueryParamConfig;
export const integrationQueryParamConfig = {
string: ["search"],
} as const satisfies QueryParamConfig;
export type IntegrationsQueryParams = QueryParamValues<
typeof integrationsQueryParamConfig
>;
export interface IntegrationsState {
search: string;
showIgnored: boolean;
showDisabled: boolean;
}
export const integrationsQueryParams = (
state: IntegrationsState
): IntegrationsQueryParams => ({
search: state.search,
status: [
...(state.showIgnored ? ["ignored"] : []),
...(state.showDisabled ? ["disabled"] : []),
],
});
// What a link says to show, undefined when it has none of the page's params
// so what was picked before stays. A link replaces the search and the toggles.
export const integrationsLinkFromUrl = (
searchParams: SearchParamsSource
): IntegrationsState | undefined => {
if (!hasQueryParams(searchParams, integrationsQueryParamConfig)) {
return undefined;
}
const { search, status } = decodeQueryParams(
searchParams,
integrationsQueryParamConfig
);
return {
search: search ?? "",
showIgnored: !!status?.includes("ignored"),
showDisabled: !!status?.includes("disabled"),
};
};
export const integrationSearchFromUrl = (
searchParams: SearchParamsSource
): string | undefined => {
if (!hasQueryParams(searchParams, integrationQueryParamConfig)) {
return undefined;
}
return (
decodeQueryParams(searchParams, integrationQueryParamConfig).search ?? ""
);
};
@@ -18,6 +18,7 @@ import { customElement, property, queryAll, state } from "lit/decorators";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import {
@@ -26,6 +27,10 @@ import {
} from "../../../common/integrations/protocolIntegrationPicked";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
integrationQueryParamConfig,
integrationSearchFromUrl,
} from "../../../common/url/integrations-query-params";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
@@ -176,6 +181,15 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
@queryAll("ha-config-entry-row")
private _configEntryRows!: NodeListOf<HaConfigEntryRow>;
private _queryParams = new QueryParamsController(
this,
integrationQueryParamConfig,
{
enabled: () => true,
navigated: () => this._applyUrl(),
}
);
private _handleHashChange = () => {
this._searchParms = new URLSearchParams(
__DEMO__ ? window.location.search : window.location.hash.substring(1)
@@ -237,6 +251,10 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
this._fetchDiagnostics();
this._fetchEntitySources();
}
if (!this.hasUpdated) {
this._applyUrl();
}
if (
changedProperties.has("configEntries") ||
changedProperties.has("_extraConfigEntries")
@@ -720,11 +738,12 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
</div>
${
normalData.length + attentionData.length > 0
normalData.length + attentionData.length > 0 || this._filter
? html`<ha-input-search
appearance="outlined"
.value=${this._filter}
@input=${this._handleSearchChange}
@blur=${this._flushUrl}
></ha-input-search>`
: nothing
}
@@ -1256,6 +1275,32 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
private _handleSearchChange(ev: HASSDomTargetEvent<HaInputSearch>) {
this._filter = ev.target.value ?? "";
this._writeUrl(true);
}
private _flushUrl() {
this._queryParams.flush();
}
private _writeUrl(debounced = false) {
this._queryParams.write({ search: this._filter }, debounced);
}
private _applyUrl() {
const searchParams = this._queryParams.searchParams();
if (!searchParams) {
return;
}
const search = integrationSearchFromUrl(searchParams);
if (search !== undefined) {
this._filter = search;
}
this._writeUrl();
}
private async _handleEnableDebugLogging() {
@@ -9,6 +9,7 @@ import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { consume } from "../../../common/decorators/consume";
import { QueryParamsController } from "../../../common/controllers/query-params-controller";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { deepActiveElement } from "../../../common/dom/deep-active-element";
import type { HASSDomTargetEvent } from "../../../common/dom/fire_event";
@@ -16,12 +17,13 @@ import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import {
getHistoryState,
navigate,
updateHistoryState,
} from "../../../common/navigate";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import {
integrationsLinkFromUrl,
integrationsQueryParamConfig,
integrationsQueryParams,
} from "../../../common/url/integrations-query-params";
import { extractSearchParam } from "../../../common/url/search-params";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
@@ -169,7 +171,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
window.location.hash.substring(1)
);
@state() private _filter: string = getHistoryState()?.filter || "";
@state() private _filter = "";
@state() private _logInfos?: Record<string, IntegrationLogInfo>;
@@ -185,6 +187,17 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
private _childReadyRegistered = false;
// The page can first be opened on /add, which sends itself on to the page
private _queryParams = new QueryParamsController(
this,
integrationsQueryParamConfig,
{
enabled: () => true,
navigated: () => this._applyUrl(),
path: "/config/integrations/dashboard",
}
);
@consume({ context: childPanelReadyContext, subscribe: true })
private _registerChildPanelReady?: RegisterChildPanelReady;
@@ -398,6 +411,14 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
}
);
protected willUpdate(changed: PropertyValues<this>) {
super.willUpdate(changed);
if (!this.hasUpdated) {
this._applyUrl();
}
}
protected firstUpdated(changed: PropertyValues<this>) {
super.firstUpdated(changed);
this._fetchManifests();
@@ -556,6 +577,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
appearance="outlined"
.value=${this._filter}
@input=${this._handleSearchChange}
@blur=${this._flushUrl}
.placeholder=${this.hass.localize(
"ui.panel.config.integrations.search"
)}
@@ -577,6 +599,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
appearance="outlined"
.value=${this._filter}
@input=${this._handleSearchChange}
@blur=${this._flushUrl}
.placeholder=${this.hass.localize(
"ui.panel.config.integrations.search"
)}
@@ -886,6 +909,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
}
private _createFlow() {
// Keep the search typed so far on the entry the dialog's Back returns to
this._queryParams.flush();
showAddIntegrationDialog(this, {
initialFilter: this._filter,
navigateToResult: true,
@@ -897,6 +922,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
switch (action) {
case "show-ignored":
this._showIgnored = !this._showIgnored;
this._writeUrl();
break;
case "show-disabled":
this._toggleShowDisabled();
@@ -906,11 +932,46 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
private _toggleShowDisabled() {
this._showDisabled = !this._showDisabled;
this._writeUrl();
}
private _handleSearchChange(ev: HASSDomTargetEvent<HaInputSearch>) {
this._filter = ev.target.value ?? "";
updateHistoryState({ filter: this._filter });
this._writeUrl(true);
}
private _flushUrl() {
this._queryParams.flush();
}
private _writeUrl(debounced = false) {
this._queryParams.write(
integrationsQueryParams({
search: this._filter,
showIgnored: this._showIgnored,
showDisabled: this._showDisabled,
}),
debounced
);
}
private _applyUrl() {
const searchParams = this._queryParams.searchParams();
if (!searchParams) {
return;
}
const link = integrationsLinkFromUrl(searchParams);
if (link) {
this._filter = link.search;
this._showIgnored = link.showIgnored;
this._showDisabled = link.showDisabled;
}
this._writeUrl();
}
private async _highlightEntry() {
@@ -945,7 +1006,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
}
const brand = extractSearchParam("brand");
const domain = extractSearchParam("domain");
navigate("/config/integrations/dashboard/", { replace: true });
navigate("/config/integrations/dashboard", { replace: true });
if (brand) {
showAddIntegrationDialog(this, {
@@ -60,6 +60,7 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
},
integration: {
tag: "ha-config-integration-page",
itemId: true,
},
},
};
@@ -201,7 +202,12 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
if (urlParams.has("domain")) {
const domain = urlParams.get("domain");
pageEl.domain = domain;
navigate(`/config/integrations/integration/${domain}`);
urlParams.delete("domain");
const search = urlParams.toString();
navigate(
`/config/integrations/integration/${domain}${search ? `?${search}` : ""}`,
{ replace: true }
);
}
}
}
+13 -2
View File
@@ -258,10 +258,15 @@ export const getMyRedirects = (): Redirects => ({
redirect: "/config/integrations/integration",
params: {
domain: "string",
search: "string?",
},
},
integrations: {
redirect: "/config/integrations",
redirect: "/config/integrations/dashboard",
params: {
search: "string?",
status: "string?",
},
},
config_mqtt: {
component: "mqtt",
@@ -847,7 +852,13 @@ class HaPanelMy extends LitElement {
this._redirect!.redirect === "/config/integrations/integration" &&
params.domain === "hacs"
) {
return `${this._redirect!.redirect}?${createSearchParam({ domain: "marketplace" })}`;
const marketplaceParams = new URLSearchParams(
this._createRedirectParams()
);
marketplaceParams.set("domain", "marketplace");
return `${this._redirect!.redirect}?${marketplaceParams}`;
}
// Special case for supervisor_app/supervisor_addon: use path-based URL