mirror of
https://github.com/home-assistant/frontend.git
synced 2026-11-07 05:23:57 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
175636d20d | ||
|
|
da47ac9c16 | ||
|
|
3c49abb492 | ||
|
|
9494db6462 | ||
|
|
06461704ad |
No files matched your search
+13
-2
@@ -16,6 +16,13 @@ declare global {
|
||||
export interface NavigateOptions {
|
||||
replace?: boolean;
|
||||
data?: Record<string, unknown>;
|
||||
/**
|
||||
* Leave open dialogs open. For a replace that only canonicalizes the current
|
||||
* URL, like a router filling in its default page: a dialog restored from the
|
||||
* URL, such as a more-info deep link, may already be open at that point, and
|
||||
* the entry keeps its state so back still closes it.
|
||||
*/
|
||||
keepDialogs?: boolean;
|
||||
}
|
||||
|
||||
// max time to wait for dialogs to close before navigating
|
||||
@@ -196,7 +203,8 @@ const buildHistoryState = (
|
||||
};
|
||||
|
||||
const performNavigation = async (path: string, options?: NavigateOptions) => {
|
||||
const canProceed = await ensureDialogsClosed(Date.now());
|
||||
const keepDialogs = options?.keepDialogs || false;
|
||||
const canProceed = await (keepDialogs || ensureDialogsClosed(Date.now()));
|
||||
if (!canProceed) {
|
||||
return false;
|
||||
}
|
||||
@@ -215,7 +223,10 @@ const performNavigation = async (path: string, options?: NavigateOptions) => {
|
||||
|
||||
const { history } = mainWindow;
|
||||
|
||||
if (replace) {
|
||||
if (replace && keepDialogs) {
|
||||
// Same entry under its canonical URL: its state, dialog included, stays.
|
||||
history.replaceState(history.state, "", path);
|
||||
} else if (replace) {
|
||||
// A replaced entry keeps its predecessor, so it keeps `from`.
|
||||
const { root, from } = history.state ?? {};
|
||||
const data = root ? { root: true } : options?.data;
|
||||
|
||||
@@ -85,6 +85,9 @@ const _removeOpenDialogTag = (dialogTag: string) => {
|
||||
export const getTopOpenDialogTag = (): string | undefined =>
|
||||
OPEN_DIALOG_TAGS[OPEN_DIALOG_TAGS.length - 1];
|
||||
|
||||
export const isDialogOpen = (dialogTag: string): boolean =>
|
||||
OPEN_DIALOG_STACK.some((dialog) => dialog.dialogTag === dialogTag);
|
||||
|
||||
/** The URL of the last dialog's entry, when the dialog went back to close. */
|
||||
let closedDialogUrl: { pathname: string; search: string } | undefined;
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ export class HassRouterPage extends ReactiveElement {
|
||||
const queryParams = window.location.search;
|
||||
navigate(`${route.prefix}/${defaultPage}${queryParams}`, {
|
||||
replace: true,
|
||||
keepDialogs: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,6 +135,7 @@ export class HassRouterPage extends ReactiveElement {
|
||||
if (route) {
|
||||
navigate(`${route.prefix}/${result}${location.search}`, {
|
||||
replace: true,
|
||||
keepDialogs: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { WindowWithPreloads } from "../data/preloads";
|
||||
import type { RecorderInfo } from "../data/recorder";
|
||||
import { getRecorderInfo } from "../data/recorder";
|
||||
import { showHttpPendingConfigDialog } from "../dialogs/http-pending-config/show-dialog-http-pending-config";
|
||||
import { isDialogOpen } from "../dialogs/make-dialog-manager";
|
||||
import { showMoreInfoDialog } from "../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../resources/custom-card-support";
|
||||
import { HassElement } from "../state/hass-element";
|
||||
@@ -327,6 +328,9 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
// Wait for the next render to ensure the view is fully loaded
|
||||
// because the more info dialog is closed when the url changes
|
||||
afterNextRender(() => {
|
||||
if (isDialogOpen("ha-more-info-dialog")) {
|
||||
return;
|
||||
}
|
||||
showMoreInfoDialog(this, {
|
||||
entityId,
|
||||
view,
|
||||
|
||||
@@ -189,7 +189,7 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
const { pathname, search, hash } = mainWindow.location;
|
||||
navigate(
|
||||
`${pathname.replace(/^\/hacs/, "/marketplace")}${search}${hash}`,
|
||||
{ replace: true }
|
||||
{ replace: true, keepDialogs: true }
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,10 @@ class HaConfigEnergy extends LitElement {
|
||||
if (changedProperties.has("route")) {
|
||||
const tab = this.route.path.substring(1);
|
||||
if (!tab || !TABS.some((t) => t.path.endsWith(`/${tab}`))) {
|
||||
navigate(`${this.route.prefix}/electricity`, { replace: true });
|
||||
navigate(`${this.route.prefix}/electricity${location.search}`, {
|
||||
replace: true,
|
||||
keepDialogs: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,10 @@ class PanelEnergy extends LitElement {
|
||||
const validPaths = views.map((view) => view.path);
|
||||
const viewPath: string | undefined = this.route!.path.split("/")[1];
|
||||
if (!viewPath || !validPaths.includes(viewPath)) {
|
||||
navigate(`${this.route!.prefix}/${validPaths[0]}`, { replace: true });
|
||||
navigate(`${this.route!.prefix}/${validPaths[0]}${location.search}`, {
|
||||
replace: true,
|
||||
keepDialogs: true,
|
||||
});
|
||||
} else {
|
||||
// Force hui-root to re-process the route by creating a new route object
|
||||
this.route = { ...this.route! };
|
||||
|
||||
@@ -291,6 +291,12 @@ export class LovelacePanel extends LitElement {
|
||||
this._fetchConfig(true);
|
||||
}
|
||||
|
||||
private _redirectToHome() {
|
||||
// Keeps the query and any open dialog: a more-info deep link may be
|
||||
// restored from the URL around this redirect.
|
||||
navigate(`/home${location.search}`, { replace: true, keepDialogs: true });
|
||||
}
|
||||
|
||||
private async _fetchConfig(forceDiskRefresh: boolean) {
|
||||
this._loading = true;
|
||||
|
||||
@@ -300,7 +306,7 @@ export class LovelacePanel extends LitElement {
|
||||
|
||||
// If no mode, redirect to /home as there is no "lovelace" dashboard
|
||||
if (!confMode) {
|
||||
navigate("/home", { replace: true });
|
||||
this._redirectToHome();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -361,7 +367,7 @@ export class LovelacePanel extends LitElement {
|
||||
const dashboards = await fetchDashboards(this.hass!);
|
||||
const dashboard = dashboards.find((d) => d.url_path === "lovelace");
|
||||
if (!dashboard) {
|
||||
navigate("/home", { replace: true });
|
||||
this._redirectToHome();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { UndoRedoController } from "../../common/controllers/undo-redo-controlle
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { ctrlOrCmdLabel } from "../../common/keyboard/ctrl-or-cmd";
|
||||
import type { NavigateOptions } from "../../common/navigate";
|
||||
import { goBack, navigate, replaceCurrentUrl } from "../../common/navigate";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import { constructUrlCurrentPath } from "../../common/url/construct-url";
|
||||
@@ -831,7 +832,12 @@ class HUIRoot extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
if (!viewPath && views.length) {
|
||||
newSelectView = views.findIndex(this._isVisible);
|
||||
this._navigateToView(views[newSelectView].path || newSelectView, true);
|
||||
// Filling in the default view keeps the entry as is: a dialog restored
|
||||
// from the URL, like a more-info deep link, can already be open.
|
||||
this._navigateToView(views[newSelectView].path || newSelectView, {
|
||||
replace: true,
|
||||
keepDialogs: true,
|
||||
});
|
||||
} else if (viewPath === "hass-unused-entities") {
|
||||
newSelectView = "hass-unused-entities";
|
||||
} else if (viewPath) {
|
||||
@@ -865,10 +871,9 @@ class HUIRoot extends KeyboardShortcutMixin(LitElement) {
|
||||
viewPath === "hass-unused-entities"
|
||||
) {
|
||||
newSelectView = views.findIndex(this._isVisible);
|
||||
this._navigateToView(
|
||||
views[newSelectView].path || newSelectView,
|
||||
true
|
||||
);
|
||||
this._navigateToView(views[newSelectView].path || newSelectView, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1197,7 +1202,7 @@ class HUIRoot extends KeyboardShortcutMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _navigateToView(path: string | number, replace?: boolean) {
|
||||
private _navigateToView(path: string | number, options?: NavigateOptions) {
|
||||
if (this.embedded) {
|
||||
fireEvent(this, "view-navigate", { path: String(path) });
|
||||
|
||||
@@ -1208,7 +1213,7 @@ class HUIRoot extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
const currentUrl = `${location.pathname}${location.search}`;
|
||||
if (currentUrl !== url) {
|
||||
navigate(url, { replace });
|
||||
navigate(url, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1278,7 +1283,7 @@ class HUIRoot extends KeyboardShortcutMixin(LitElement) {
|
||||
newCurView = indexAfterMove(curView, oldIndex, newIndex);
|
||||
// Views without a path are addressed by their index in the URL
|
||||
if (newCurView !== curView && !this.config.views[curView].path) {
|
||||
this._navigateToView(newCurView, true);
|
||||
this._navigateToView(newCurView, { replace: true });
|
||||
}
|
||||
}
|
||||
lovelace.saveConfig(moveView(lovelace.config, oldIndex, newIndex));
|
||||
|
||||
@@ -191,7 +191,10 @@ class PanelMediaBrowser extends LitElement {
|
||||
}
|
||||
|
||||
if (this.route.path === "") {
|
||||
navigate(`/media-browser/${this._entityId}`, { replace: true });
|
||||
navigate(`/media-browser/${this._entityId}${location.search}`, {
|
||||
replace: true,
|
||||
keepDialogs: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,10 +12,15 @@ import {
|
||||
registerUnsavedChangesGuard,
|
||||
unregisterUnsavedChangesGuard,
|
||||
} from "../../src/common/navigate";
|
||||
import { closeAllDialogs } from "../../src/dialogs/make-dialog-manager";
|
||||
|
||||
// navigate() closes open dialogs before touching history.
|
||||
// navigate() closes open dialogs before touching history. A closed dialog
|
||||
// drops its state from the entry, which is what navigate() waits for.
|
||||
vi.mock("../../src/dialogs/make-dialog-manager", () => ({
|
||||
closeAllDialogs: vi.fn(async () => true),
|
||||
closeAllDialogs: vi.fn(async () => {
|
||||
window.history.replaceState(null, "");
|
||||
return true;
|
||||
}),
|
||||
}));
|
||||
|
||||
// Fabricates a raw entry like a document load, which navigate() never produces.
|
||||
@@ -56,6 +61,7 @@ const trackDeferredGuard = (isDirty: () => boolean = () => true) => {
|
||||
|
||||
afterEach(() => {
|
||||
registeredGuards.splice(0).forEach(unregisterUnsavedChangesGuard);
|
||||
vi.mocked(closeAllDialogs).mockClear();
|
||||
});
|
||||
|
||||
describe("navigate", () => {
|
||||
@@ -97,6 +103,41 @@ describe("navigate", () => {
|
||||
expect(window.history.state).toBeNull();
|
||||
expect(canGoBack()).toBe(false);
|
||||
});
|
||||
|
||||
it("closes an open dialog before replacing the entry", async () => {
|
||||
setEntry("/home", { dialog: "ha-more-info-dialog" });
|
||||
|
||||
await navigate("/home/overview", { replace: true });
|
||||
|
||||
expect(closeAllDialogs).toHaveBeenCalledOnce();
|
||||
expect(window.location.pathname).toEqual("/home/overview");
|
||||
expect(window.history.state).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps an open dialog and the entry state when asked to", async () => {
|
||||
setEntry("/home", { dialog: "ha-more-info-dialog", from: "/" });
|
||||
|
||||
await navigate("/home/overview", { replace: true, keepDialogs: true });
|
||||
|
||||
expect(closeAllDialogs).not.toHaveBeenCalled();
|
||||
expect(window.location.pathname).toEqual("/home/overview");
|
||||
expect(window.history.state).toEqual({
|
||||
dialog: "ha-more-info-dialog",
|
||||
from: "/",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not drop a navigation right after one that keeps dialogs", async () => {
|
||||
setEntry("/home");
|
||||
|
||||
const results = await Promise.all([
|
||||
navigate("/home/overview", { replace: true, keepDialogs: true }),
|
||||
navigate("/config"),
|
||||
]);
|
||||
|
||||
expect(results).toEqual([true, true]);
|
||||
expect(window.location.pathname).toEqual("/config");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsaved changes guard", () => {
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("partial-panel-resolver", () => {
|
||||
// The Marketplace loads its translations for the path it is at
|
||||
expect(navigate).toHaveBeenCalledWith(
|
||||
"/marketplace/repository/1?category=theme#top",
|
||||
{ replace: true }
|
||||
{ replace: true, keepDialogs: true }
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user