diff --git a/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.logic.ts b/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.logic.ts new file mode 100644 index 00000000000..72224c30aed --- /dev/null +++ b/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.logic.ts @@ -0,0 +1,31 @@ +/** + * Extracts the layout value from a URL query string. + * Returns null if no valid layout is present. + */ +export function getLayoutFromUrl(search: string, validLayouts: string[]): string | null { + try { + const params = new URLSearchParams(search); + const layout = params.get("layout"); + + if (!layout) return null; + if (!validLayouts.includes(layout)) return null; + + return layout; + } catch { + return null; + } +} + +/** + * Returns a new query string with the layout updated. + * Does not touch window or history — pure logic only. + */ +export function setLayoutInQuery(search: string, layout: string): string { + try { + const params = new URLSearchParams(search); + params.set("layout", layout); + return params.toString(); + } catch { + return search; + } +} diff --git a/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx b/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx index 6b8f857b1a6..d66eec1a9b3 100644 --- a/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx +++ b/apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.tsx @@ -14,6 +14,8 @@ import { cn } from "@plane/utils"; import { IssueLayoutIcon } from "@/components/issues/issue-layouts/layout-icon"; // hooks import { usePlatformOS } from "@/hooks/use-platform-os"; +import { useEffect } from "react"; +import { getLayoutFromUrl, setLayoutInQuery } from "./layout-selection.logic"; type Props = { layouts: EIssueLayoutTypes[]; @@ -25,9 +27,27 @@ export function LayoutSelection(props: Props) { const { layouts, onChange, selectedLayout } = props; const { isMobile } = usePlatformOS(); const { t } = useTranslation(); + + // Read layout from URL once on mount and apply if valid + useEffect(() => { + if (typeof window === "undefined") return; + + const layout = getLayoutFromUrl(window.location.search, layouts); + if (layout && (layout as EIssueLayoutTypes) !== selectedLayout) { + onChange(layout as EIssueLayoutTypes); + } + }, [layouts, onChange, selectedLayout]); + const handleOnChange = (layoutKey: EIssueLayoutTypes) => { if (selectedLayout !== layoutKey) { onChange(layoutKey); + + if (typeof window !== "undefined") { + const newQuery = setLayoutInQuery(window.location.search, layoutKey); + const newUrl = newQuery ? `${window.location.pathname}?${newQuery}` : window.location.pathname; + + window.history.replaceState({}, "", newUrl); + } } }; diff --git a/packages/codemods/tests/layout-selection.logic.test.ts b/packages/codemods/tests/layout-selection.logic.test.ts new file mode 100644 index 00000000000..a2b86e0c05a --- /dev/null +++ b/packages/codemods/tests/layout-selection.logic.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from "vitest"; +import { + getLayoutFromUrl, + setLayoutInQuery, +} from "../../../apps/web/core/components/issues/issue-layouts/filters/header/layout-selection.logic"; + +describe("layout-selection.logic", () => { + describe("getLayoutFromUrl", () => { + it("returns the layout when valid", () => { + const result = getLayoutFromUrl("?layout=list", ["list", "kanban"]); + expect(result).toBe("list"); + }); + + it("returns null when layout is missing", () => { + const result = getLayoutFromUrl("?foo=bar", ["list"]); + expect(result).toBeNull(); + }); + + it("returns null when layout is invalid", () => { + const result = getLayoutFromUrl("?layout=unknown", ["list"]); + expect(result).toBeNull(); + }); + }); + + describe("setLayoutInQuery", () => { + it("updates the layout in the query string", () => { + const result = setLayoutInQuery("?foo=bar", "list"); + expect(result).toBe("foo=bar&layout=list"); + }); + + it("overwrites an existing layout", () => { + const result = setLayoutInQuery("?layout=kanban", "list"); + expect(result).toBe("layout=list"); + }); + }); +});