diff --git a/src/components/MultiSelectToolbar.tsx b/src/components/MultiSelectToolbar.tsx
index ef9c57ac..132883e7 100644
--- a/src/components/MultiSelectToolbar.tsx
+++ b/src/components/MultiSelectToolbar.tsx
@@ -22,6 +22,11 @@ export function MultiSelectToolbar() {
[selectedNodes]
);
+ const hasSelectedRegularNode = useMemo(
+ () => selectedNodes.some((node) => node.type !== "group"),
+ [selectedNodes]
+ );
+
// Check if any selected nodes are in a group
const selectedNodeGroups = useMemo(() => {
const groupIds = new Set(selectedNodes.map((n) => n.groupId).filter(Boolean));
@@ -150,7 +155,9 @@ export function MultiSelectToolbar() {
removeNodesFromGroup(nodeIds);
};
- if (!toolbarPosition || selectedNodes.length < 2) return null;
+ const shouldHideForRegroupIntent = hasSelectedGroupNode && hasSelectedRegularNode;
+
+ if (!toolbarPosition || selectedNodes.length < 2 || shouldHideForRegroupIntent) return null;
return (
{
expect(mockCreateGroup).toHaveBeenCalledWith(["node-1", "node-2"]);
});
- it("should show create group button when a selected group is included", () => {
+ it("should not render when selected nodes include a group and regular nodes", () => {
mockUseWorkflowStore.mockImplementation((selector) => {
return selector(createDefaultState({
nodes: [
@@ -418,17 +418,15 @@ describe("MultiSelectToolbar", () => {
}));
});
- render(
+ const { container } = render(
);
- const createGroupButton = screen.getByTitle("创建分组");
- fireEvent.click(createGroupButton);
-
+ expect(container.firstChild).toBeNull();
+ expect(screen.queryByTitle("创建分组")).not.toBeInTheDocument();
expect(screen.queryByTitle("从分组中移除")).not.toBeInTheDocument();
- expect(mockCreateGroup).toHaveBeenCalledWith(["group-node-group-1", "node-3"]);
});
it("should show ungroup button when selected nodes are in a group", () => {
diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx
index 05651351..2f073213 100644
--- a/src/components/composer/GenerationComposer.tsx
+++ b/src/components/composer/GenerationComposer.tsx
@@ -12,10 +12,9 @@ import {
type MouseEvent,
type ReactNode,
} from "react";
-import { createPortal } from "react-dom";
import { CloseOutlined, DownOutlined, FileTextOutlined, RightOutlined } from "@ant-design/icons";
import { useReactFlow, useViewport, ViewportPortal } from "@xyflow/react";
-import { message, Modal, Popover, Segmented, Tooltip } from "antd";
+import { Dropdown, message, Modal, Popover, Segmented, Tooltip } from "antd";
import { useProviderApiKeys, useWorkflowStore } from "@/store/workflowStore";
import { useModelStore } from "@/store/modelStore";
import {
@@ -2891,49 +2890,74 @@ export function GenerationComposer() {
return t("composer.describeContent");
}, [connectedInputs, context.mode, context.selectedCount, isEaseCurveNode, isVideoStitchNode, promptForSubmit, t]);
+ useEffect(() => {
+ if (shouldRenderInlineComposer) return;
+ assistTriggerIndexRef.current = null;
+ setAssistMenu(null);
+ setAssistMenuPosition(null);
+ }, [shouldRenderInlineComposer]);
+
const hideComposerForModal = isModalOpen && !modelDialogOpen;
- const assistMenuOverlay = assistMenu && assistMenuPosition && typeof document !== "undefined"
- ? createPortal(
-
{
+ if (nextOpen) return;
+ assistTriggerIndexRef.current = null;
+ setAssistMenu(null);
+ setAssistMenuPosition(null);
}}
- onPointerDownCapture={stopCanvasEvent}
- onMouseDownCapture={stopCanvasEvent}
- onClick={stopCanvasEvent}
- onWheelCapture={stopCanvasEvent}
+ trigger={["click"]}
+ placement="top"
+ overlayClassName="z-[10080]"
+ menu={{ items: [] }}
+ popupRender={() => (
+
+
+ {assistMenu === "command" ? t("composer.commands") : t("composer.referenceMaterials")}
+
+
+ {activeAssistItems.map((item, itemIndex) => {
+ const isActive = itemIndex === assistMenuActiveIndex;
+ return (
+
+ );
+ })}
+
+
+ )}
>
-
- {assistMenu === "command" ? t("composer.commands") : t("composer.referenceMaterials")}
-
-
- {activeAssistItems.map((item, itemIndex) => {
- const isActive = itemIndex === assistMenuActiveIndex;
- return (
-
- );
- })}
-
-
,
- document.body
+
+
)
: null;
diff --git a/src/components/nodes/GroupNode.tsx b/src/components/nodes/GroupNode.tsx
index 52a0d18a..837baf91 100644
--- a/src/components/nodes/GroupNode.tsx
+++ b/src/components/nodes/GroupNode.tsx
@@ -10,7 +10,7 @@ import {
Position,
} from "@xyflow/react";
import { DownloadOutlined } from "@ant-design/icons";
-import { Modal } from "antd";
+import { Dropdown, Modal, Popover, type MenuProps } from "antd";
import { useI18n } from "@/i18n";
import type { TranslationKey } from "@/i18n";
import { GROUP_COLORS, useWorkflowStore } from "@/store/workflowStore";
@@ -60,11 +60,10 @@ export function GroupNode({ id, data, selected }: NodeProps
) {
const imageRefBasePath = useWorkflowStore((state) => state.imageRefBasePath ?? null);
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(group?.name || "");
- const [activePopover, setActivePopover] = useState<"color" | "layout" | null>(null);
+ const [colorPopoverOpen, setColorPopoverOpen] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const inputRef = useRef(null);
- const toolbarRef = useRef(null);
useEffect(() => {
if (!isEditing) setEditName(group?.name || "");
@@ -77,18 +76,6 @@ export function GroupNode({ id, data, selected }: NodeProps) {
}
}, [isEditing]);
- useEffect(() => {
- if (!activePopover) return;
-
- const handleClickOutside = (event: MouseEvent) => {
- if (toolbarRef.current?.contains(event.target as Node)) return;
- setActivePopover(null);
- };
-
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, [activePopover]);
-
const handleNameSubmit = useCallback(() => {
const nextName = editName.trim();
if (nextName && nextName !== group?.name) {
@@ -116,7 +103,6 @@ export function GroupNode({ id, data, selected }: NodeProps) {
const groupNodes = nodes.filter((node) => node.type !== "group" && node.groupId === groupId);
if (groupNodes.length < 2) {
updateGroup(groupId, { layout });
- setActivePopover(null);
return;
}
@@ -165,7 +151,6 @@ export function GroupNode({ id, data, selected }: NodeProps) {
updateGroup(groupId, { layout });
onNodesChange(changes);
- setActivePopover(null);
},
[groupId, nodes, onNodesChange, updateGroup]
);
@@ -238,6 +223,37 @@ export function GroupNode({ id, data, selected }: NodeProps) {
const bgColor = GROUP_COLORS[group.color];
const showResizeControls = selected || isHovered;
+ const colorPopoverContent = (
+ event.stopPropagation()}
+ onPointerDown={(event) => event.stopPropagation()}
+ >
+ {COLOR_OPTIONS.map(({ color, labelKey }) => (
+
+ );
+ const layoutMenuItems: MenuProps["items"] = LAYOUT_OPTIONS.map(({ layout, labelKey }) => ({
+ key: layout,
+ label: (
+
+ {t(labelKey)}
+
+ ),
+ }));
return (
<>
@@ -250,50 +266,45 @@ export function GroupNode({ id, data, selected }: NodeProps) {
>
-
+
-
- {activePopover === "color" && (
-
- {COLOR_OPTIONS.map(({ color, labelKey }) => (
-
- )}
-
-
-
+
+
+
{
+ domEvent.stopPropagation();
+ handleApplyLayout(key as GroupLayout);
+ },
+ }}
+ >
- {activePopover === "layout" && (
-
- {LAYOUT_OPTIONS.map(({ layout, labelKey }) => (
-
- ))}
-
- )}
-
+