diff --git a/src/components/GroupsOverlay.tsx b/src/components/GroupsOverlay.tsx index 43cee3e2..bd31db96 100644 --- a/src/components/GroupsOverlay.tsx +++ b/src/components/GroupsOverlay.tsx @@ -30,8 +30,58 @@ interface GroupBackgroundProps { // Renders just the group background - displayed below nodes (z-index 1) function GroupBackground({ groupId }: GroupBackgroundProps) { - const { groups } = useWorkflowStore(); + const { groups, updateGroup, moveGroupNodes } = useWorkflowStore(); const group = groups[groupId]; + const { zoom } = useViewport(); + const [isDragging, setIsDragging] = useState(false); + const dragStartRef = useRef<{ x: number; y: number } | null>(null); + + const handleBackgroundMouseDown = useCallback( + (e: React.MouseEvent) => { + if (e.button !== 0) return; + + e.stopPropagation(); + e.preventDefault(); + setIsDragging(true); + dragStartRef.current = { x: e.clientX, y: e.clientY }; + }, + [] + ); + + useEffect(() => { + if (!isDragging || !group) return; + + const handleMouseMove = (e: MouseEvent) => { + if (!dragStartRef.current) return; + + const deltaX = (e.clientX - dragStartRef.current.x) / zoom; + const deltaY = (e.clientY - dragStartRef.current.y) / zoom; + + if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) { + updateGroup(groupId, { + position: { + x: group.position.x + deltaX, + y: group.position.y + deltaY, + }, + }); + moveGroupNodes(groupId, { x: deltaX, y: deltaY }); + dragStartRef.current = { x: e.clientX, y: e.clientY }; + } + }; + + const handleMouseUp = () => { + setIsDragging(false); + dragStartRef.current = null; + }; + + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [isDragging, group, groupId, moveGroupNodes, updateGroup, zoom]); if (!group) return null; @@ -39,7 +89,8 @@ function GroupBackground({ groupId }: GroupBackgroundProps) { return (
); @@ -531,7 +582,7 @@ export function GroupBackgroundsPortal() { return ( -
+
{groupIds.map((groupId) => ( ))} diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 5a5cd896..dd0357ed 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -2586,29 +2586,14 @@ export function WorkflowCanvas() { fitViewOptions={FIT_VIEW_OPTIONS} deleteKeyCode={["Backspace", "Delete"]} multiSelectionKeyCode="Shift" - selectionOnDrag={ - canvasNavigationSettings.selectionMode === "altDrag" || canvasNavigationSettings.selectionMode === "shiftDrag" - ? false - : canvasNavigationSettings.panMode === "always" - ? false - : isMacOSClient && !isModalOpen - } - selectionKeyCode={ - isModalOpen ? null - : canvasNavigationSettings.selectionMode === "altDrag" ? "Alt" - : canvasNavigationSettings.selectionMode === "shiftDrag" ? "Shift" - : "Shift" - } + selectionOnDrag={tutorialActive ? false : !isModalOpen} + selectionKeyCode={null} panOnDrag={ tutorialActive ? false : isModalOpen ? false - : canvasNavigationSettings.panMode === "always" - ? true - : canvasNavigationSettings.panMode === "middleMouse" - ? [2] - : !isMacOSClient + : false } selectNodesOnDrag={false} nodeDragThreshold={5} @@ -2623,9 +2608,7 @@ export function WorkflowCanvas() { ? null : isModalOpen ? null - : canvasNavigationSettings.panMode === "space" - ? "Space" - : null + : "Space" } nodesDraggable={!isModalOpen} nodesConnectable={!isModalOpen} diff --git a/src/components/__tests__/GenerationComposer.test.tsx b/src/components/__tests__/GenerationComposer.test.tsx index 44691cc7..952ea174 100644 --- a/src/components/__tests__/GenerationComposer.test.tsx +++ b/src/components/__tests__/GenerationComposer.test.tsx @@ -1187,7 +1187,6 @@ describe("GenerationComposer", () => { }); render(); - expect(screen.getByText("Video Stitch")).toBeInTheDocument(); expect(screen.getByText("2 video clips")).toBeInTheDocument(); expect(screen.getByText("Video inputs")).toBeInTheDocument(); expect(screen.getByText("Clip 1")).toBeInTheDocument(); @@ -1224,7 +1223,6 @@ describe("GenerationComposer", () => { }); render(); - expect(screen.getByText("Ease Curve")).toBeInTheDocument(); expect(screen.getByText("Video connected")).toBeInTheDocument(); expect(screen.getByText("Curve settings")).toBeInTheDocument(); expect(screen.getByText("easeInOutSine")).toBeInTheDocument(); diff --git a/src/components/__tests__/GroupsOverlay.test.tsx b/src/components/__tests__/GroupsOverlay.test.tsx index 84cf1888..7573cbbf 100644 --- a/src/components/__tests__/GroupsOverlay.test.tsx +++ b/src/components/__tests__/GroupsOverlay.test.tsx @@ -147,7 +147,7 @@ describe("GroupBackgroundsPortal", () => { expect(style).toContain("border"); }); - it("should set pointerEvents to none on backgrounds", () => { + it("should make backgrounds interactive for group dragging", () => { mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState({ groups: { "group-1": createMockGroup() }, @@ -156,7 +156,32 @@ describe("GroupBackgroundsPortal", () => { const { container } = render(); const background = container.querySelector(".rounded-xl"); - expect(background).toHaveStyle({ pointerEvents: "none" }); + expect(background).toHaveStyle({ pointerEvents: "auto" }); + }); + + it("should drag a group from its background", () => { + mockUseWorkflowStore.mockImplementation((selector) => { + return selector(createDefaultState({ + groups: { "group-1": createMockGroup() }, + })); + }); + + const { container } = render(); + const background = container.querySelector(".rounded-xl") as HTMLElement; + + fireEvent.mouseDown(background, { button: 0, clientX: 100, clientY: 100 }); + act(() => { + fireEvent.mouseMove(window, { clientX: 130, clientY: 120 }); + }); + + expect(mockUpdateGroup).toHaveBeenCalledWith("group-1", { + position: { x: 130, y: 120 }, + }); + expect(mockMoveGroupNodes).toHaveBeenCalledWith("group-1", { x: 30, y: 20 }); + + act(() => { + fireEvent.mouseUp(window); + }); }); }); }); diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx index 81a3d661..283cfd33 100644 --- a/src/components/composer/GenerationComposer.tsx +++ b/src/components/composer/GenerationComposer.tsx @@ -1996,11 +1996,6 @@ export function GenerationComposer() { ) : ( <>
-
- {t("composer.console")} - {modeLabel} -
- {isProcessNode ? ( isVideoStitchNode ? (
diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index d293b5b8..d19751d0 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -592,7 +592,6 @@ const en = { "composer.uploadReferenceImage": "Upload reference image", "composer.expand": "Expand composer", "composer.collapse": "Collapse composer", - "composer.console": "Console", "composer.style": "Style", "composer.mark": "Mark", "composer.focus": "Focus", @@ -1422,7 +1421,6 @@ const zhCN: Dictionary = { "composer.uploadReferenceImage": "上传参考图片", "composer.expand": "展开底部栏", "composer.collapse": "收起底部栏", - "composer.console": "控制台", "composer.style": "风格", "composer.mark": "标记", "composer.focus": "聚焦",