From f90563b35ca0d7ff6fe1b54f87c998fffeb1d8c2 Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Tue, 7 Jul 2026 11:45:01 +0300 Subject: [PATCH] fix(flows): wrap add-node menu groups in DropdownMenuGroup (#336) Clicking "Add node" in the flow builder crashed the whole page to a "This page couldn't load" error boundary, in both the list and canvas views. The add-node menus render a DropdownMenuLabel per category. That is base-ui's Menu.GroupLabel, which calls useMenuGroupRootContext() during render and THROWS ("MenuGroupContext is missing") when it has no Menu.Group ancestor. Both menus wrapped each category in a plain
instead of a DropdownMenuGroup, so the label had no group context. The menu content only mounts when the menu opens, which is why initial render was fine and the crash fired exactly on click. Wrap each category's label + items in DropdownMenuGroup (base-ui Menu.Group), which provides the context Menu.GroupLabel needs. The between-group separator moves into a keyed Fragment so it stays a sibling of the groups. Adds a regression test pinning the contract: a bare DropdownMenuLabel throws, a group-wrapped one does not. Co-Authored-By: Claude Opus 4.8 --- src/components/flows/flow-builder.tsx | 37 +++++----- src/components/flows/flow-canvas.tsx | 67 ++++++++++--------- .../ui/dropdown-menu-group-label.test.tsx | 36 ++++++++++ 3 files changed, 95 insertions(+), 45 deletions(-) create mode 100644 src/components/ui/dropdown-menu-group-label.test.tsx diff --git a/src/components/flows/flow-builder.tsx b/src/components/flows/flow-builder.tsx index d644d76..ae459da 100644 --- a/src/components/flows/flow-builder.tsx +++ b/src/components/flows/flow-builder.tsx @@ -17,7 +17,7 @@ * are list-only and have no canvas analogue. */ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CircleAlert, Plus, @@ -39,6 +39,7 @@ import { import { DropdownMenu, DropdownMenuContent, + DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, @@ -589,21 +590,27 @@ function AddNodeButton({ onAdd }: { onAdd: (type: NodeType) => void }) { {groupNodeTypesByCategory(types).map((group, i) => ( -
+ // A DropdownMenuGroup (base-ui Menu.Group) is REQUIRED here: + // DropdownMenuLabel is base-ui's Menu.GroupLabel, which throws + // at render if it can't find a Menu.Group ancestor context. A + // plain
wrapper made opening this menu crash the page. + {i > 0 && } - - {group.label} - - {group.types.map((t) => { - const meta = NODE_META[t]; - return ( - onAdd(t)}> - - {meta.label} - - ); - })} -
+ + + {group.label} + + {group.types.map((t) => { + const meta = NODE_META[t]; + return ( + onAdd(t)}> + + {meta.label} + + ); + })} + + ))} diff --git a/src/components/flows/flow-canvas.tsx b/src/components/flows/flow-canvas.tsx index 29bea17..10e668b 100644 --- a/src/components/flows/flow-canvas.tsx +++ b/src/components/flows/flow-canvas.tsx @@ -36,7 +36,7 @@ * list view reads. */ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { applyNodeChanges, Background, @@ -87,6 +87,7 @@ import { import { DropdownMenu, DropdownMenuContent, + DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, @@ -737,37 +738,43 @@ function CanvasAddNodeButton() { className="border-border bg-popover w-[268px] p-1.5" > {groupNodeTypesByCategory(ADD_NODE_TYPES).map((group, i) => ( -
+ // DropdownMenuGroup (base-ui Menu.Group) is REQUIRED: the + // DropdownMenuLabel below is base-ui's Menu.GroupLabel, which + // throws at render without a Menu.Group ancestor. A plain
+ // here crashed the page when this menu opened (issue #336). + {i > 0 && } - - {group.label} - - {group.types.map((t) => { - const meta = NODE_META[t]; - return ( - handleAdd(t)} - className="gap-3 py-2" - > - - - - {meta.label} + + + {group.label} + + {group.types.map((t) => { + const meta = NODE_META[t]; + return ( + handleAdd(t)} + className="gap-3 py-2" + > + + + + {meta.label} + + + {meta.blurb} + - - {meta.blurb} - - - - ); - })} -
+ + ); + })} + + ))} diff --git a/src/components/ui/dropdown-menu-group-label.test.tsx b/src/components/ui/dropdown-menu-group-label.test.tsx new file mode 100644 index 0000000..998c366 --- /dev/null +++ b/src/components/ui/dropdown-menu-group-label.test.tsx @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest' +import React from 'react' +import { renderToStaticMarkup } from 'react-dom/server' + +import { DropdownMenuGroup, DropdownMenuLabel } from './dropdown-menu' + +/** + * Regression for issue #336 — "clicking Add node reloads the page". + * + * DropdownMenuLabel is base-ui's Menu.GroupLabel, which reads a required + * Menu.Group context and THROWS at render when it's missing. The flow + * builder's add-node menu wrapped its labels in a plain
instead of + * a DropdownMenuGroup, so opening the menu crashed the whole page. These + * tests pin the contract so the div-wrapper regression can't come back. + */ +describe('DropdownMenuLabel requires a DropdownMenuGroup ancestor', () => { + it('throws when rendered without a group (the #336 crash)', () => { + expect(() => + renderToStaticMarkup( + React.createElement(DropdownMenuLabel, null, 'Messaging'), + ), + ).toThrow() + }) + + it('renders when wrapped in a DropdownMenuGroup (the fix)', () => { + expect(() => + renderToStaticMarkup( + React.createElement( + DropdownMenuGroup, + null, + React.createElement(DropdownMenuLabel, null, 'Messaging'), + ), + ), + ).not.toThrow() + }) +})