fix: keep mobile board widgets unchanged

This commit is contained in:
ajnart committed 2026-07-29 20:21:59 +02:00
1 parent e2732dce2c
commit b5ffefe707
43 files changed
+1669 -181

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
import { describe, expect, test } from "vitest";
import type { RouterOutputs } from "@homarr/api";
import { getBoardLayouts, getCurrentLayout, getDesktopLayout } from "./context";
const createBoard = () =>
({
layouts: [
{ id: "small", name: "Small", columnCount: 4, breakpoint: 0 },
{ id: "medium", name: "Medium", columnCount: 8, breakpoint: 768 },
{ id: "large", name: "Large", columnCount: 12, breakpoint: 1200 },
],
}) as RouterOutputs["board"]["getBoardByName"];
describe("board layout selection", () => {
test("automatic mode always uses the desktop layout", () => {
const board = createBoard();
expect(getDesktopLayout(board).id).toBe("large");
expect(getCurrentLayout(board, true, 390)).toBe("large");
});
test.each([
[390, "small"],
[768, "medium"],
[1024, "medium"],
[1440, "large"],
])("legacy mode selects the matching responsive layout at %spx", (viewportWidth, expectedLayoutId) => {
expect(getCurrentLayout(createBoard(), false, viewportWidth)).toBe(expectedLayoutId);
});
test("legacy mode falls back to the desktop layout before the client viewport is known", () => {
expect(getCurrentLayout(createBoard(), false, undefined)).toBe("large");
});
test("rejects boards without a layout", () => {
const board = { ...createBoard(), layouts: [] };
expect(() => getDesktopLayout(board)).toThrow("Board must have a layout");
expect(() => getCurrentLayout(board, false, 390)).toThrow("Board must have a layout");
});
test("keeps every saved layout available for reversible mutations", () => {
expect(getBoardLayouts(createBoard())).toEqual(["small", "medium", "large"]);
});
});
+52 -7
View File
@@ -1,12 +1,13 @@
"use client";
import type { PropsWithChildren } from "react";
import { createContext, useContext, useEffect } from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { getActiveQueryCacheBoardId, setActiveQueryCacheBoardId } from "@homarr/api/query-cache";
import { useSettings } from "@homarr/settings";
import { updateBoardName } from "./updater";
@@ -73,10 +74,6 @@ export const useOptionalBoard = () => {
return context?.board ?? null;
};
export const getCurrentLayout = (board: RouterOutputs["board"]["getBoardByName"]) => {
return getDesktopLayout(board).id;
};
export const getDesktopLayout = (board: RouterOutputs["board"]["getBoardByName"]) => {
const layout = board.layouts
.toSorted(
@@ -91,9 +88,57 @@ export const getDesktopLayout = (board: RouterOutputs["board"]["getBoardByName"]
return layout;
};
export const useCurrentLayout = () => getCurrentLayout(useRequiredBoard());
export const getCurrentLayout = (
board: RouterOutputs["board"]["getBoardByName"],
enableAutomaticMobileLayout: boolean,
viewportWidth?: number,
) => {
if (enableAutomaticMobileLayout || viewportWidth === undefined) {
return getDesktopLayout(board).id;
}
export const getBoardLayouts = (board: RouterOutputs["board"]["getBoardByName"]) => [getCurrentLayout(board)];
const sortedLayouts = board.layouts.toSorted((layoutA, layoutB) => layoutB.breakpoint - layoutA.breakpoint);
const layout = sortedLayouts.find((candidate) => candidate.breakpoint <= viewportWidth) ?? sortedLayouts.at(-1);
if (!layout) {
throw new Error("Board must have a layout");
}
return layout.id;
};
export const useCurrentLayout = () => {
const board = useRequiredBoard();
const { enableAutomaticMobileLayout } = useSettings();
const [currentLayout, setCurrentLayout] = useState(() =>
getCurrentLayout(
board,
enableAutomaticMobileLayout,
!enableAutomaticMobileLayout && typeof window !== "undefined" ? window.innerWidth : undefined,
),
);
const updateCurrentLayout = useCallback(() => {
setCurrentLayout(getCurrentLayout(board, enableAutomaticMobileLayout, window.innerWidth));
}, [board, enableAutomaticMobileLayout]);
useEffect(() => {
updateCurrentLayout();
if (enableAutomaticMobileLayout) return;
window.addEventListener("resize", updateCurrentLayout);
return () => {
window.removeEventListener("resize", updateCurrentLayout);
};
}, [enableAutomaticMobileLayout, updateCurrentLayout]);
return currentLayout;
};
export const getBoardLayouts = (board: RouterOutputs["board"]["getBoardByName"]) =>
board.layouts.map((layout) => layout.id);
export const useLayouts = () => {
const board = useRequiredBoard();