fix: guard recursive section rendering

This commit is contained in:
ajnart committed 2026-08-08 18:24:38 +02:00
1 parent a9fc3c6faa
commit bc41125b7f
4 files changed
+33 -4

No files matched your search

@@ -6,6 +6,7 @@ import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
import type { DynamicSectionItem } from "~/app/[locale]/boards/_types";
import { BoardDynamicSectionMenu } from "./dynamic/dynamic-menu";
import { GridStack } from "./gridstack/gridstack";
import { useSectionContext } from "./section-context";
import classes from "./item.module.css";
interface Props {
@@ -15,6 +16,7 @@ interface Props {
export const BoardDynamicSection = ({ section }: Props) => {
const board = useRequiredBoard();
const currentLayoutId = useCurrentLayout();
const { ancestorSectionIds } = useSectionContext();
const options = section.options;
return (
@@ -53,7 +55,12 @@ export const BoardDynamicSection = ({ section }: Props) => {
</Badge>
)}
{/* Use unique key by layout to reinitialize gridstack */}
<GridStack key={`${currentLayoutId}-${section.id}`} section={section} className="min-row" />
<GridStack
key={`${currentLayoutId}-${section.id}`}
section={section}
ancestorSectionIds={ancestorSectionIds}
className="min-row"
/>
</Card>
<BoardDynamicSectionMenu section={section} />
</Box>
@@ -0,0 +1,11 @@
import { describe, expect, test } from "vitest";
import { filterRecursiveInnerSections } from "./gridstack";
describe("filterRecursiveInnerSections", () => {
test("removes sections that would recursively render an ancestor", () => {
const innerSections = [{ id: "safe" }, { id: "parent" }, { id: "root" }];
expect(filterRecursiveInnerSections(innerSections, new Set(["root", "parent"]))).toEqual([{ id: "safe" }]);
});
});
@@ -12,16 +12,21 @@ import { useGridstack } from "./use-gridstack";
interface Props extends BoxProps {
section: Exclude<Section, { kind: "dynamic" }> | DynamicSectionItem;
ancestorSectionIds?: ReadonlySet<string>;
}
export const GridStack = ({ section, ...props }: Props) => {
export const GridStack = ({ section, ancestorSectionIds = new Set(), ...props }: Props) => {
const { items, innerSections } = useSectionItems(section.id);
const itemIds = [...items, ...innerSections].map((item) => item.id);
const nextAncestorSectionIds = new Set(ancestorSectionIds).add(section.id);
const safeInnerSections = filterRecursiveInnerSections(innerSections, nextAncestorSectionIds);
const itemIds = [...items, ...safeInnerSections].map((item) => item.id);
const { refs } = useGridstack(section, itemIds);
return (
<SectionProvider value={{ section, items, innerSections, refs }}>
<SectionProvider
value={{ section, ancestorSectionIds: nextAncestorSectionIds, items, innerSections: safeInnerSections, refs }}
>
<Box
{...props}
data-kind={section.kind}
@@ -34,3 +39,8 @@ export const GridStack = ({ section, ...props }: Props) => {
</SectionProvider>
);
};
export const filterRecursiveInnerSections = <T extends { id: string }>(
innerSections: T[],
ancestorSectionIds: ReadonlySet<string>,
) => innerSections.filter((innerSection) => !ancestorSectionIds.has(innerSection.id));
@@ -5,6 +5,7 @@ import type { UseGridstackRefs } from "./gridstack/use-gridstack";
interface SectionContextProps {
section: Exclude<Section, { kind: "dynamic" }> | DynamicSectionItem;
ancestorSectionIds: ReadonlySet<string>;
innerSections: DynamicSectionItem[];
items: SectionItem[];
refs: UseGridstackRefs;