fix: honor requested board layout edits

This commit is contained in:
ajnart committed 2026-08-14 11:32:09 +02:00
1 parent eb918b805a
commit 854b59ede4
2 files changed
+108 -3

No files matched your search

@@ -0,0 +1,107 @@
import { createElement, act } from "react";
import { createRoot } from "react-dom/client";
import type { Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { BoardProviders } from "./_providers";
const mocks = vi.hoisted(() => ({
searchParams: new URLSearchParams(),
layoutOverrideId: undefined as string | null | undefined,
initialOpen: undefined as boolean | undefined,
}));
vi.mock("next/navigation", () => ({
useSearchParams: () => mocks.searchParams,
}));
vi.mock("@homarr/boards/context", async () => {
const React = await import("react");
return {
BoardProvider: ({ children, layoutOverrideId }: { children: React.ReactNode; layoutOverrideId: string | null }) => {
mocks.layoutOverrideId = layoutOverrideId;
return React.createElement(React.Fragment, null, children);
},
};
});
vi.mock("@homarr/boards/edit-mode", async () => {
const React = await import("react");
return {
EditModeProvider: ({ children, initialOpen }: { children: React.ReactNode; initialOpen: boolean }) => {
mocks.initialOpen = initialOpen;
return React.createElement(React.Fragment, null, children);
},
};
});
describe("board providers", () => {
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
mocks.layoutOverrideId = undefined;
mocks.initialOpen = undefined;
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
test.each([
["base", "base"],
["custom", "custom"],
])("uses the requested %s layout for an explicit edit request", (_name, requestedLayoutId) => {
mocks.searchParams = new URLSearchParams(`layout=${requestedLayoutId}&edit=true&returnTo=settings`);
act(() => {
root.render(
createElement(
BoardProviders,
{
initialBoard: { layouts: [{ id: "mobile" }, { id: "custom" }, { id: "base" }] } as never,
initialLayoutId: "mobile",
initialViewportWidth: 390,
canModify: true,
},
createElement("span", null, "board"),
),
);
});
expect(mocks.layoutOverrideId).toBe(requestedLayoutId);
expect(mocks.initialOpen).toBe(true);
});
test.each([
["without edit mode", "layout=base", true],
["for an unknown layout", "layout=unknown&edit=true", true],
["without modify access", "layout=base&edit=true", false],
])("keeps viewport layout selection %s", (_name, query, canModify) => {
mocks.searchParams = new URLSearchParams(query);
act(() => {
root.render(
createElement(
BoardProviders,
{
initialBoard: { layouts: [{ id: "mobile" }, { id: "base" }] } as never,
initialLayoutId: "mobile",
initialViewportWidth: 390,
canModify,
},
createElement("span", null, "board"),
),
);
});
expect(mocks.layoutOverrideId).toBeNull();
expect(mocks.initialOpen).toBe(false);
});
});
@@ -2,7 +2,6 @@
import type { PropsWithChildren } from "react";
import { useSearchParams } from "next/navigation";
import { useMediaQuery } from "@mantine/hooks";
import { BoardProvider } from "@homarr/boards/context";
import { EditModeProvider } from "@homarr/boards/edit-mode";
@@ -24,11 +23,10 @@ export const BoardProviders = ({
canModify,
}: PropsWithChildren<Props>) => {
const searchParams = useSearchParams();
const isDesktop = useMediaQuery("(min-width: 64em)");
const requestedLayoutId = searchParams.get("layout");
const isRequestedLayoutValid = initialBoard.layouts.some((layout) => layout.id === requestedLayoutId);
const isLayoutEditRequest = canModify && searchParams.get("edit") === "true" && isRequestedLayoutValid;
const layoutOverrideId = isLayoutEditRequest && isDesktop ? requestedLayoutId : null;
const layoutOverrideId = isLayoutEditRequest ? requestedLayoutId : null;
return (
<BoardProvider