mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-29 02:16:15 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c37fa57e12 |
@@ -10,18 +10,15 @@ const VIEW_PARAM = "more-info-view";
|
||||
export interface MoreInfoUrlData {
|
||||
entityId?: string;
|
||||
view?: MoreInfoView;
|
||||
hash: URLSearchParams;
|
||||
}
|
||||
|
||||
export interface CreateMoreInfoUrlData {
|
||||
entityId: string;
|
||||
view: MoreInfoView;
|
||||
hash?: URLSearchParams;
|
||||
}
|
||||
|
||||
export const decodeMoreInfoUrl = (
|
||||
search: SearchParamsSource,
|
||||
hash = ""
|
||||
search: SearchParamsSource
|
||||
): MoreInfoUrlData => {
|
||||
const params =
|
||||
typeof search === "string"
|
||||
@@ -35,9 +32,6 @@ export const decodeMoreInfoUrl = (
|
||||
return {
|
||||
entityId,
|
||||
view: isMoreInfoView(view) ? view : undefined,
|
||||
hash: new URLSearchParams(
|
||||
__DEMO__ ? "" : hash.startsWith("#") ? hash.substring(1) : hash
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -48,9 +42,6 @@ export const createMoreInfoUrl = (
|
||||
const url = new URL(base, window.location.origin);
|
||||
url.searchParams.set(ENTITY_ID_PARAM, data.entityId);
|
||||
url.searchParams.set(VIEW_PARAM, data.view);
|
||||
if (!__DEMO__ && data.hash !== undefined) {
|
||||
url.hash = data.hash.toString();
|
||||
}
|
||||
|
||||
return `${url.pathname}${url.search}${url.hash}`;
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createContext } from "@lit/context";
|
||||
|
||||
export interface MoreInfoContext {
|
||||
hash: URLSearchParams;
|
||||
setHashParam: (key: string, value?: string) => void;
|
||||
}
|
||||
|
||||
export const moreInfoContext =
|
||||
createContext<MoreInfoContext>("more-info-context");
|
||||
@@ -33,7 +33,6 @@ import type {
|
||||
WeatherEntity,
|
||||
} from "../../../data/weather";
|
||||
import {
|
||||
getDefaultForecastType,
|
||||
getForecast,
|
||||
getSecondaryWeatherAttribute,
|
||||
getSupportedForecastTypes,
|
||||
@@ -49,16 +48,11 @@ import type {
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../../types";
|
||||
import { moreInfoContext, type MoreInfoContext } from "../context";
|
||||
|
||||
@customElement("more-info-weather")
|
||||
class MoreInfoWeather extends LitElement {
|
||||
@property({ attribute: false }) public stateObj?: WeatherEntity;
|
||||
|
||||
@state()
|
||||
@consume({ context: moreInfoContext, subscribe: true })
|
||||
private _moreInfoContext?: MoreInfoContext;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
@@ -82,7 +76,9 @@ class MoreInfoWeather extends LitElement {
|
||||
|
||||
@state() private _forecastType?: ModernForecastType;
|
||||
|
||||
@state() private _subscribed?: Promise<() => void>;
|
||||
private _subscribed?: Promise<() => void>;
|
||||
|
||||
private _subscribedTo?: string;
|
||||
|
||||
private _dragScrollController = new DragScrollController(this, {
|
||||
selector: ".forecast",
|
||||
@@ -94,24 +90,30 @@ class MoreInfoWeather extends LitElement {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
this._subscribedTo = undefined;
|
||||
this._forecastEvent = undefined;
|
||||
}
|
||||
|
||||
private async _subscribeForecastEvents() {
|
||||
this._unsubscribeForecastEvents();
|
||||
if (
|
||||
!this.isConnected ||
|
||||
!this._connection ||
|
||||
!this.stateObj ||
|
||||
!this._forecastType
|
||||
) {
|
||||
private _updateForecastSubscription() {
|
||||
const stateObj = this.stateObj;
|
||||
const forecastType = this._forecastType;
|
||||
|
||||
if (!this.isConnected || !this._connection || !stateObj || !forecastType) {
|
||||
this._unsubscribeForecastEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
const target = `${stateObj.entity_id}-${forecastType}`;
|
||||
if (target === this._subscribedTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._unsubscribeForecastEvents();
|
||||
this._subscribedTo = target;
|
||||
this._subscribed = subscribeForecast(
|
||||
this._connection.connection,
|
||||
this.stateObj.entity_id,
|
||||
this._forecastType,
|
||||
stateObj.entity_id,
|
||||
forecastType,
|
||||
(event) => {
|
||||
this._forecastEvent = event;
|
||||
}
|
||||
@@ -121,7 +123,7 @@ class MoreInfoWeather extends LitElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._subscribeForecastEvents();
|
||||
this._updateForecastSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,37 +135,9 @@ class MoreInfoWeather extends LitElement {
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
(changedProps.has("stateObj") ||
|
||||
changedProps.has("_moreInfoContext") ||
|
||||
!this._subscribed) &&
|
||||
this.stateObj
|
||||
) {
|
||||
const oldState = changedProps.get("stateObj") as
|
||||
WeatherEntity | undefined;
|
||||
if (
|
||||
oldState?.entity_id !== this.stateObj?.entity_id ||
|
||||
changedProps.has("_moreInfoContext") ||
|
||||
!this._subscribed
|
||||
) {
|
||||
const supportedForecastTypes = getSupportedForecastTypes(this.stateObj);
|
||||
const requestedForecastType =
|
||||
this._moreInfoContext?.hash.get("forecast");
|
||||
const selectedForecastType =
|
||||
supportedForecastTypes.find(
|
||||
(forecastType) => forecastType === requestedForecastType
|
||||
) ?? getDefaultForecastType(this.stateObj);
|
||||
if (selectedForecastType !== requestedForecastType) {
|
||||
this._moreInfoContext?.setHashParam("forecast", selectedForecastType);
|
||||
}
|
||||
if (this._forecastType !== selectedForecastType || !this._subscribed) {
|
||||
this._forecastType = selectedForecastType;
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
}
|
||||
} else if (changedProps.has("_forecastType")) {
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
this._forecastType = this._selectedForecastType();
|
||||
|
||||
this._updateForecastSubscription();
|
||||
}
|
||||
|
||||
protected updated(_changedProps: PropertyValues<this>): void {
|
||||
@@ -184,6 +158,16 @@ class MoreInfoWeather extends LitElement {
|
||||
getSupportedForecastTypes(stateObj)
|
||||
);
|
||||
|
||||
private _selectedForecastType(): ModernForecastType | undefined {
|
||||
if (!this.stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
const supported = this._supportedForecasts(this.stateObj);
|
||||
return (
|
||||
supported.find((type) => type === this._forecastType) ?? supported[0]
|
||||
);
|
||||
}
|
||||
|
||||
private _groupForecastByDay = memoizeOne((forecast: ForecastAttribute[]) => {
|
||||
if (!forecast) return [];
|
||||
|
||||
@@ -536,14 +520,7 @@ class MoreInfoWeather extends LitElement {
|
||||
private _handleForecastTypeChanged(
|
||||
ev: HASSDomEvent<{ name: ModernForecastType }>
|
||||
): void {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
!getSupportedForecastTypes(this.stateObj).includes(ev.detail.name)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._forecastType = ev.detail.name;
|
||||
this._moreInfoContext?.setHashParam("forecast", this._forecastType);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
mdiTransitConnectionVariant,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { provide } from "@lit/context";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -92,7 +91,6 @@ import {
|
||||
EDITABLE_DOMAINS_WITH_UNIQUE_ID,
|
||||
type MoreInfoView,
|
||||
} from "./const";
|
||||
import { moreInfoContext, type MoreInfoContext } from "./context";
|
||||
import "./controls/more-info-default";
|
||||
import type { FavoritesDialogContext } from "./favorites";
|
||||
import { getFavoritesDialogHandler } from "./favorites";
|
||||
@@ -110,7 +108,6 @@ export interface MoreInfoDialogParams {
|
||||
tab?: MoreInfoView;
|
||||
large?: boolean;
|
||||
data?: Record<string, any>;
|
||||
hash?: URLSearchParams;
|
||||
fromUrl?: boolean;
|
||||
returnUrl?: string;
|
||||
parentElement?: LitElement;
|
||||
@@ -158,10 +155,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
|
||||
@state() private _data?: Record<string, any>;
|
||||
|
||||
@provide({ context: moreInfoContext })
|
||||
@state()
|
||||
private _moreInfoContext: MoreInfoContext = this._createMoreInfoContext();
|
||||
|
||||
private _returnUrl?: string;
|
||||
|
||||
@state() private _currView: MoreInfoView = DEFAULT_VIEW;
|
||||
@@ -198,7 +191,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
const view = params.view || params.tab || DEFAULT_VIEW;
|
||||
|
||||
this._data = params.data;
|
||||
this._moreInfoContext = this._createMoreInfoContext(params.hash);
|
||||
this._returnUrl = params.returnUrl;
|
||||
this._currView = view;
|
||||
this._initialView = view;
|
||||
@@ -252,7 +244,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
this._initialView = DEFAULT_VIEW;
|
||||
this._currView = DEFAULT_VIEW;
|
||||
this._childViewStack = [];
|
||||
this._moreInfoContext = this._createMoreInfoContext();
|
||||
this._returnUrl = undefined;
|
||||
this._isEscapeEnabled = true;
|
||||
window.removeEventListener("dialog-closed", this._enableEscapeKeyClose);
|
||||
@@ -301,10 +292,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
return entity?.device_id ?? null;
|
||||
}
|
||||
|
||||
private _setView(view: MoreInfoView, preserveHash = false) {
|
||||
if (view !== this._currView && !preserveHash) {
|
||||
this._moreInfoContext = this._createMoreInfoContext();
|
||||
}
|
||||
private _setView(view: MoreInfoView) {
|
||||
updateHistoryState({
|
||||
dialogParams: {
|
||||
...getHistoryState()?.dialogParams,
|
||||
@@ -323,29 +311,10 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
createMoreInfoUrl(this._returnUrl, {
|
||||
entityId: this._entityId,
|
||||
view: this._currView,
|
||||
hash: this._moreInfoContext.hash,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _createMoreInfoContext(hash?: URLSearchParams): MoreInfoContext {
|
||||
return {
|
||||
hash: new URLSearchParams(hash),
|
||||
setHashParam: (key, value) => this._setHashParam(key, value),
|
||||
};
|
||||
}
|
||||
|
||||
private _setHashParam(key: string, value?: string) {
|
||||
const hash = new URLSearchParams(this._moreInfoContext.hash);
|
||||
if (value) {
|
||||
hash.set(key, value);
|
||||
} else {
|
||||
hash.delete(key);
|
||||
}
|
||||
this._moreInfoContext = this._createMoreInfoContext(hash);
|
||||
this._syncUrl();
|
||||
}
|
||||
|
||||
private _goBack() {
|
||||
if (this._childView) {
|
||||
const dialog =
|
||||
@@ -371,7 +340,6 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
if (this._parentEntityIds.length > 0) {
|
||||
this._entityId = this._parentEntityIds.pop();
|
||||
this._currView = DEFAULT_VIEW;
|
||||
this._moreInfoContext = this._createMoreInfoContext();
|
||||
this._loadEntityRegistryEntry();
|
||||
this._syncUrl();
|
||||
}
|
||||
@@ -1078,15 +1046,13 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
}
|
||||
const view = ev.detail.view || ev.detail.tab || DEFAULT_VIEW;
|
||||
if (entityId === this._entityId) {
|
||||
this._moreInfoContext = this._createMoreInfoContext(ev.detail.hash);
|
||||
this._infoEditMode = false;
|
||||
this._detailsYamlMode = false;
|
||||
this._setView(view, true);
|
||||
this._setView(view);
|
||||
return;
|
||||
}
|
||||
this._parentEntityIds = [...this._parentEntityIds, this._entityId!];
|
||||
this._entityId = entityId;
|
||||
this._moreInfoContext = this._createMoreInfoContext(ev.detail.hash);
|
||||
this._currView = view === "details" ? view : DEFAULT_VIEW;
|
||||
this._initialView = view;
|
||||
this._infoEditMode = false;
|
||||
|
||||
@@ -318,10 +318,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
if (!searchParams["more-info-entity-id"]) {
|
||||
return;
|
||||
}
|
||||
const { entityId, view, hash } = decodeMoreInfoUrl(
|
||||
window.location.search,
|
||||
window.location.hash
|
||||
);
|
||||
const { entityId, view } = decodeMoreInfoUrl(window.location.search);
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
@@ -331,7 +328,6 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId,
|
||||
view,
|
||||
hash,
|
||||
fromUrl: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,6 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
)
|
||||
: false),
|
||||
data: ev.detail.data,
|
||||
hash: ev.detail.hash,
|
||||
returnUrl,
|
||||
},
|
||||
() => import("../dialogs/more-info/ha-more-info-dialog"),
|
||||
@@ -66,7 +65,6 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
createMoreInfoUrl(returnUrl, {
|
||||
entityId: ev.detail.entityId,
|
||||
view,
|
||||
hash: ev.detail.hash,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -247,15 +247,13 @@ describe("todo query params", () => {
|
||||
});
|
||||
|
||||
describe("more-info query params", () => {
|
||||
it("decodes the entity, view, and named hash params", () => {
|
||||
it("decodes the entity and view", () => {
|
||||
const params = decodeMoreInfoUrl(
|
||||
"?more-info-entity-id=weather.home&more-info-view=info",
|
||||
"#forecast=hourly"
|
||||
"?more-info-entity-id=weather.home&more-info-view=info"
|
||||
);
|
||||
|
||||
expect(params.entityId).toBe("weather.home");
|
||||
expect(params.view).toBe("info");
|
||||
expect(params.hash.get("forecast")).toBe("hourly");
|
||||
});
|
||||
|
||||
it("ignores invalid views", () => {
|
||||
@@ -271,22 +269,21 @@ describe("more-info query params", () => {
|
||||
createMoreInfoUrl("/lovelace/home?theme=dark", {
|
||||
entityId: "weather.home",
|
||||
view: "info",
|
||||
hash: new URLSearchParams({ forecast: "hourly" }),
|
||||
})
|
||||
).toBe(
|
||||
"/lovelace/home?theme=dark&more-info-entity-id=weather.home&more-info-view=info#forecast=hourly"
|
||||
"/lovelace/home?theme=dark&more-info-entity-id=weather.home&more-info-view=info"
|
||||
);
|
||||
});
|
||||
|
||||
it("removes more-info query params but preserves other hash state", () => {
|
||||
it("removes more-info query params but preserves the hash", () => {
|
||||
expect(
|
||||
removeMoreInfoUrl(
|
||||
"/lovelace/home?theme=dark&more-info-entity-id=weather.home&more-info-view=info#forecast=hourly"
|
||||
"/lovelace/home?theme=dark&more-info-entity-id=weather.home&more-info-view=info#some-anchor"
|
||||
)
|
||||
).toBe("/lovelace/home?theme=dark#forecast=hourly");
|
||||
).toBe("/lovelace/home?theme=dark#some-anchor");
|
||||
});
|
||||
|
||||
it("preserves an unrelated hash when creating a more-info url without a dialog hash", () => {
|
||||
it("preserves the hash of the page it links from", () => {
|
||||
expect(
|
||||
createMoreInfoUrl("/lovelace/home?theme=dark#some-anchor", {
|
||||
entityId: "light.kitchen",
|
||||
@@ -296,16 +293,4 @@ describe("more-info query params", () => {
|
||||
"/lovelace/home?theme=dark&more-info-entity-id=light.kitchen&more-info-view=info#some-anchor"
|
||||
);
|
||||
});
|
||||
|
||||
it("clears an existing hash when an empty dialog hash is explicitly supplied", () => {
|
||||
expect(
|
||||
createMoreInfoUrl("/lovelace/home?theme=dark#some-anchor", {
|
||||
entityId: "light.kitchen",
|
||||
view: "info",
|
||||
hash: new URLSearchParams(),
|
||||
})
|
||||
).toBe(
|
||||
"/lovelace/home?theme=dark&more-info-entity-id=light.kitchen&more-info-view=info"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+14
-27
@@ -389,8 +389,10 @@ test.describe("Light more-info dialog", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test.describe("Weather more-info deep link", () => {
|
||||
test("opens and synchronizes the selected forecast", async ({ page }) => {
|
||||
test.describe("Weather more-info forecast", () => {
|
||||
test("switches the rendered forecast when a tab is selected", async ({
|
||||
page,
|
||||
}) => {
|
||||
await goToPanel(
|
||||
page,
|
||||
"/?scenario=weather-more-info&more-info-entity-id=weather.test_weather&more-info-view=info#/lovelace"
|
||||
@@ -406,40 +408,25 @@ test.describe("Weather more-info deep link", () => {
|
||||
weather.locator("ha-tab-group-tab[active]").filter({ hasText: "Daily" })
|
||||
).toBeAttached();
|
||||
|
||||
await page.locator("ha-test").evaluate((el) => {
|
||||
el.dispatchEvent(
|
||||
new CustomEvent("hass-more-info", {
|
||||
detail: {
|
||||
entityId: "weather.test_weather",
|
||||
hash: new URLSearchParams({ forecast: "hourly" }),
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
// Only the hourly and twice daily forecasts group their items under a day
|
||||
// header, so it tells the rendered forecast apart from the daily one.
|
||||
await expect(weather.locator(".forecast-day-header")).toHaveCount(0);
|
||||
|
||||
await weather
|
||||
.locator("ha-tab-group-tab")
|
||||
.filter({ hasText: "Hourly" })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
weather.locator("ha-tab-group-tab[active]").filter({ hasText: "Hourly" })
|
||||
).toBeAttached();
|
||||
await expect(weather.locator(".forecast-day-header").first()).toBeVisible();
|
||||
|
||||
await dialog.getByRole("button", { name: "History" }).click();
|
||||
await expect(page).toHaveURL(/more-info-view=history/);
|
||||
|
||||
await dialog.getByRole("button", { name: "Back" }).click();
|
||||
|
||||
await expect(
|
||||
weather.locator("ha-tab-group-tab[active]").filter({ hasText: "Daily" })
|
||||
).toBeAttached();
|
||||
|
||||
await weather
|
||||
.locator("ha-tab-group-tab")
|
||||
.filter({ hasText: "Daily" })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
weather.locator("ha-tab-group-tab[active]").filter({ hasText: "Daily" })
|
||||
).toBeAttached();
|
||||
await expect(page).toHaveURL(/more-info-view=info/);
|
||||
|
||||
await dialog.getByRole("button", { name: "Close" }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
|
||||
Reference in New Issue
Block a user